Skip to content

Commit 83abc29

Browse files
committed
Friendship ended with tuples, now named tuple is my new best friend
1 parent 09dc6e0 commit 83abc29

15 files changed

Lines changed: 178 additions & 132 deletions

File tree

‎README.md‎

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,12 @@ await client.outbound.send_batch([email1, email2, email3])
132132

133133
```python
134134
# Paginated list
135-
messages, total = await client.outbound.list(
135+
page = await client.outbound.list(
136136
count=50,
137137
recipient="user@example.com",
138138
tag="onboarding",
139139
)
140-
print(f"Found {total} messages")
140+
print(f"Found {page.total} messages, got {len(page.items)} this page")
141141

142142
# Auto-paginating stream
143143
async for message in client.outbound.stream(max_messages=5000, tag="onboarding"):
@@ -155,28 +155,29 @@ print(dump.body)
155155

156156
```python
157157
# List open events across all messages
158-
opens, total = await client.outbound.list_opens(count=50, tag="onboarding")
158+
page = await client.outbound.list_opens(count=50, tag="onboarding")
159+
print(f"{page.total} total opens, {len(page.items)} returned")
159160

160161
# List open events for a specific message
161-
opens, total = await client.outbound.list_message_opens(message_id)
162+
page = await client.outbound.list_message_opens(message_id)
162163

163164
# List click events across all messages
164-
clicks, total = await client.outbound.list_clicks(count=50, tag="onboarding")
165+
page = await client.outbound.list_clicks(count=50, tag="onboarding")
165166

166167
# List click events for a specific message
167-
clicks, total = await client.outbound.list_message_clicks(message_id)
168+
page = await client.outbound.list_message_clicks(message_id)
168169
```
169170

170171
### Inbound Messages
171172

172173
```python
173174
# List inbound messages (with optional filters)
174-
messages, total = await client.inbound.list(
175+
page = await client.inbound.list(
175176
count=50,
176177
status="processed",
177178
tag="support",
178179
)
179-
print(f"Found {total} inbound messages")
180+
print(f"Found {page.total} inbound messages")
180181

181182
# Get full details for a single inbound message
182183
details = await client.inbound.get(message_id)
@@ -198,7 +199,7 @@ stats = await client.bounces.get_delivery_stats()
198199
print(f"Inactive addresses: {stats.inactive_mails}")
199200

200201
# List bounces
201-
bounces, total = await client.bounces.list(
202+
page = await client.bounces.list(
202203
type=BounceType.HARD_BOUNCE,
203204
count=50,
204205
)
@@ -220,7 +221,7 @@ result = await client.bounces.activate(bounce_id)
220221
from postmark.models.templates import TemplateTypeFilter
221222

222223
# List templates
223-
templates, total = await client.templates.list()
224+
page = await client.templates.list()
224225

225226
# Get / create / edit / delete
226227
template = await client.templates.get(template_id)
@@ -243,6 +244,36 @@ await client.outbound.send_batch_with_template([
243244
])
244245
```
245246

247+
### Message Streams
248+
249+
```python
250+
from postmark.models.streams import MessageStreamType
251+
252+
# List all streams
253+
page = await client.streams.list()
254+
print(f"{page.total} streams")
255+
256+
# Filter by type
257+
page = await client.streams.list(message_stream_type=MessageStreamType.TRANSACTIONAL)
258+
259+
# Get a specific stream
260+
stream = await client.streams.get("outbound")
261+
262+
# Create a stream
263+
stream = await client.streams.create(
264+
id="my-broadcasts",
265+
name="My Broadcasts",
266+
message_stream_type=MessageStreamType.BROADCASTS,
267+
)
268+
269+
# Edit a stream
270+
stream = await client.streams.edit("my-broadcasts", name="Renamed Stream")
271+
272+
# Archive / unarchive
273+
await client.streams.archive("my-broadcasts")
274+
await client.streams.unarchive("my-broadcasts")
275+
```
276+
246277
### Server Settings (server token)
247278

248279
Read and update the settings of the server associated with your server token:
@@ -270,7 +301,7 @@ Create, list, and manage all servers on your account:
270301
from postmark.models.servers import DeliveryType, ServerColor
271302

272303
# List all servers
273-
servers, total = await account.server.list()
304+
page = await account.server.list()
274305

275306
# Get a specific server
276307
server = await account.server.get(server_id)

‎postmark/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
)
1515
from .models import messages
1616
from .models.messages import Attachment, Email, Header, Message, SendResponse
17+
from .models.page import Page
1718
from .utils import server_utils
1819

1920
# Base logger
@@ -38,4 +39,5 @@
3839
"ServerException",
3940
"TimeoutException",
4041
"server_utils",
42+
"Page",
4143
]

‎postmark/models/bounces/manager.py‎

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import logging
22
from datetime import datetime
3-
from typing import AsyncGenerator, List, Optional, Tuple
3+
from typing import AsyncGenerator, Optional
44

5+
from postmark.models.page import Page
56
from postmark.utils.types import HTTPClient
67

78
from .enums import BounceType
@@ -39,7 +40,7 @@ async def list(
3940
from_date: Optional[datetime] = None,
4041
to_date: Optional[datetime] = None,
4142
message_stream: Optional[str] = None,
42-
) -> Tuple[List[Bounce], int]:
43+
) -> Page[Bounce]:
4344
"""
4445
List bounces for the server.
4546
@@ -55,9 +56,6 @@ async def list(
5556
from_date: Return bounces on or after this date (Eastern Time).
5657
to_date: Return bounces on or before this date (Eastern Time).
5758
message_stream: Filter by message stream ID.
58-
59-
Returns:
60-
A ``(bounces, total_count)`` tuple.
6159
"""
6260
if count > 500:
6361
raise ValueError("count cannot exceed 500")
@@ -85,7 +83,7 @@ async def list(
8583

8684
response = await self.client.get("/bounces", params=params)
8785
data = BouncesListResponse(**response.json())
88-
return data.bounces, data.total_count
86+
return Page(items=data.bounces, total=data.total_count)
8987

9088
async def stream(
9189
self,
@@ -116,20 +114,18 @@ async def stream(
116114
if current_limit <= 0:
117115
break
118116

119-
bounces, total = await self.list(
120-
count=current_limit, offset=offset, **filters
121-
)
122-
if not bounces:
117+
page = await self.list(count=current_limit, offset=offset, **filters)
118+
if not page.items:
123119
break
124120

125-
for bounce in bounces:
121+
for bounce in page.items:
126122
yield bounce
127123
yielded += 1
128124
if yielded >= max_bounces:
129125
return
130126

131-
offset += len(bounces)
132-
if offset >= total:
127+
offset += len(page.items)
128+
if offset >= page.total:
133129
break
134130

135131
async def get(self, bounce_id: int) -> Bounce:

‎postmark/models/inbound/manager.py‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
from typing import Any, Dict, List, Tuple
1+
from typing import Any, Dict
22

3+
from postmark.models.page import Page
34
from postmark.utils.types import HTTPClient
45

56
from .schemas import InboundActionResponse, InboundMessage, InboundMessageDetails
@@ -14,7 +15,7 @@ async def list(
1415
count: int = 100,
1516
offset: int = 0,
1617
**filters,
17-
) -> Tuple[List[InboundMessage], int]:
18+
) -> Page[InboundMessage]:
1819
"""
1920
List inbound messages.
2021
@@ -23,9 +24,6 @@ async def list(
2324
offset: Number of records to skip.
2425
**filters: recipient, fromemail, tag, subject, mailboxhash, status,
2526
todate, fromdate.
26-
27-
Returns:
28-
A ``(messages, total_count)`` tuple.
2927
"""
3028
if count > 500:
3129
raise ValueError("Count cannot exceed 500 per request")
@@ -39,9 +37,9 @@ async def list(
3937

4038
response = await self.client.get("/messages/inbound", params=params)
4139
data = response.json()
42-
return (
43-
[InboundMessage(**m) for m in data.get("InboundMessages", [])],
44-
data.get("TotalCount", 0),
40+
return Page(
41+
items=[InboundMessage(**m) for m in data.get("InboundMessages", [])],
42+
total=data.get("TotalCount", 0),
4543
)
4644

4745
async def get(self, message_id: str) -> InboundMessageDetails:

‎postmark/models/messages/manager.py‎

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from pydantic import ValidationError
66

77
from postmark.exceptions import InvalidEmailException
8+
from postmark.models.page import Page
89
from postmark.models.templates.schemas import TemplateEmail
910
from postmark.utils.types import HTTPClient
1011

@@ -197,7 +198,7 @@ async def list(
197198
offset: int = 0,
198199
metadata: Optional[Dict[str, str]] = None,
199200
**filters,
200-
) -> tuple[List[Message], int]:
201+
) -> Page[Message]:
201202
"""List sent messages."""
202203
if count > 500:
203204
raise ValueError("Count cannot exceed 500 messages per request")
@@ -222,8 +223,9 @@ async def list(
222223
response.raise_for_status()
223224
data = response.json()
224225

225-
return [Message(**msg) for msg in data.get("Messages", [])], data.get(
226-
"TotalCount", 0
226+
return Page(
227+
items=[Message(**msg) for msg in data.get("Messages", [])],
228+
total=data.get("TotalCount", 0),
227229
)
228230

229231
async def stream(
@@ -246,20 +248,18 @@ async def stream(
246248
if current_limit <= 0:
247249
break
248250

249-
messages, total = await self.list(
250-
count=current_limit, offset=offset, **filters
251-
)
252-
if not messages:
251+
page = await self.list(count=current_limit, offset=offset, **filters)
252+
if not page.items:
253253
break
254254

255-
for msg in messages:
255+
for msg in page.items:
256256
yield msg
257257
yielded += 1
258258
if yielded >= max_messages:
259259
return
260260

261-
offset += len(messages)
262-
if offset >= total:
261+
offset += len(page.items)
262+
if offset >= page.total:
263263
break
264264

265265
async def get(self, message_id: str) -> MessageDetails:
@@ -278,7 +278,7 @@ async def get_dump(self, message_id: str) -> OutboundMessageDump:
278278

279279
async def list_opens(
280280
self, count: int = 100, offset: int = 0, **filters
281-
) -> tuple[List[OpenEvent], int]:
281+
) -> Page[OpenEvent]:
282282
"""
283283
List open tracking events across all messages.
284284
@@ -304,13 +304,14 @@ async def list_opens(
304304

305305
response = await self.client.get("/messages/outbound/opens", params=params)
306306
data = response.json()
307-
return [OpenEvent(**o) for o in data.get("Opens", [])], data.get(
308-
"TotalCount", 0
307+
return Page(
308+
items=[OpenEvent(**o) for o in data.get("Opens", [])],
309+
total=data.get("TotalCount", 0),
309310
)
310311

311312
async def list_message_opens(
312313
self, message_id: str, count: int = 100, offset: int = 0
313-
) -> tuple[List[OpenEvent], int]:
314+
) -> Page[OpenEvent]:
314315
"""
315316
List open tracking events for a specific message.
316317
@@ -327,8 +328,9 @@ async def list_message_opens(
327328
f"/messages/outbound/opens/{message_id}", params=params
328329
)
329330
data = response.json()
330-
return [OpenEvent(**o) for o in data.get("Opens", [])], data.get(
331-
"TotalCount", 0
331+
return Page(
332+
items=[OpenEvent(**o) for o in data.get("Opens", [])],
333+
total=data.get("TotalCount", 0),
332334
)
333335

334336
# -------------------------------------------------------------------------
@@ -337,7 +339,7 @@ async def list_message_opens(
337339

338340
async def list_clicks(
339341
self, count: int = 100, offset: int = 0, **filters
340-
) -> tuple[List[ClickEvent], int]:
342+
) -> Page[ClickEvent]:
341343
"""
342344
List click tracking events across all messages.
343345
@@ -363,13 +365,14 @@ async def list_clicks(
363365

364366
response = await self.client.get("/messages/outbound/clicks", params=params)
365367
data = response.json()
366-
return [ClickEvent(**c) for c in data.get("Clicks", [])], data.get(
367-
"TotalCount", 0
368+
return Page(
369+
items=[ClickEvent(**c) for c in data.get("Clicks", [])],
370+
total=data.get("TotalCount", 0),
368371
)
369372

370373
async def list_message_clicks(
371374
self, message_id: str, count: int = 100, offset: int = 0
372-
) -> tuple[List[ClickEvent], int]:
375+
) -> Page[ClickEvent]:
373376
"""
374377
List click tracking events for a specific message.
375378
@@ -386,6 +389,7 @@ async def list_message_clicks(
386389
f"/messages/outbound/clicks/{message_id}", params=params
387390
)
388391
data = response.json()
389-
return [ClickEvent(**c) for c in data.get("Clicks", [])], data.get(
390-
"TotalCount", 0
392+
return Page(
393+
items=[ClickEvent(**c) for c in data.get("Clicks", [])],
394+
total=data.get("TotalCount", 0),
391395
)

‎postmark/models/page.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from dataclasses import dataclass
2+
from typing import Generic, List, TypeVar
3+
4+
T = TypeVar("T")
5+
6+
7+
@dataclass(frozen=True)
8+
class Page(Generic[T]):
9+
"""
10+
A single page of results from a paginated list endpoint.
11+
12+
Attributes:
13+
items: The list of results for this page.
14+
total: The total number of records matching the query (across all pages).
15+
"""
16+
17+
items: List[T]
18+
total: int

0 commit comments

Comments
 (0)