Skip to content

Commit 7c77192

Browse files
committed
Remove circular imports, flatten imports, simplify testing
1 parent 2007fe0 commit 7c77192

13 files changed

Lines changed: 484 additions & 522 deletions

File tree

‎examples/get_messages.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ async def list_example():
1616
server = postmark.ServerClient(server_token=server_token)
1717

1818
try:
19-
messages, total = await server.messages.Outbound.list(count=10)
19+
messages, total = await server.outbound.list(count=10)
2020

2121
print(f"Total matches on server: {total}")
2222
print(f"Retrieved in this page: {len(messages)}")
@@ -48,7 +48,7 @@ async def stream_example():
4848
count = 0
4949

5050
# Get 20 messages
51-
async for msg in server.messages.Outbound.stream(max_messages=20):
51+
async for msg in server.outbound.stream(max_messages=20):
5252
count += 1
5353

5454
# Print details for the first 3...
@@ -76,7 +76,7 @@ async def get_details_example(message_id: str):
7676

7777
try:
7878
# Get message details
79-
message = await server.messages.Outbound.get(message_id)
79+
message = await server.outbound.get(message_id)
8080

8181
print("\n--- Message Details ---")
8282
print(f"Message ID: {message.message_id}")

‎examples/send_messages.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ async def send_single_email_using_json():
1919
print("--- Sending Single Email (JSON) ---")
2020
try:
2121
# You can pass a dictionary matching the API fields
22-
response = await server.messages.Outbound.send(
22+
response = await server.outbound.send(
2323
{
2424
"From": send_email,
2525
"To": "receiver@example.com",
@@ -56,7 +56,7 @@ async def send_single_email_using_model():
5656
metadata={"user_id": "12345"},
5757
)
5858

59-
response = await server.messages.Outbound.send(email)
59+
response = await server.outbound.send(email)
6060

6161
print(f"Email sent! Message ID: {response.message_id}")
6262

@@ -89,7 +89,7 @@ async def send_batch_emails():
8989
},
9090
]
9191

92-
responses = await server.messages.Outbound.send_batch(messages)
92+
responses = await server.outbound.send_batch(messages)
9393

9494
print(f"Batch processed. Sent {len(responses)} emails.")
9595
for i, resp in enumerate(responses):

‎postmark/clients/server_client.py‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import json
22
import logging
33
import os
4-
from typing import Optional, Dict, Any, TYPE_CHECKING
4+
from typing import Optional, Dict, Any
5+
from postmark.models.messages import OutboundManager
56

67
import httpx
78

@@ -13,8 +14,6 @@
1314
get_exception_class,
1415
)
1516

16-
if TYPE_CHECKING:
17-
from postmark.models.messages import MessageService
1817

1918
logger = logging.getLogger(__name__)
2019

@@ -41,11 +40,8 @@ def __init__(self, server_token: str):
4140
if not self.verify_ssl:
4241
logger.warning("SSL verification is disabled. Do not use in production!")
4342

44-
# Initializing the import here, inside __init__ to avoid potential circular import issues at the module level
45-
# if messages.py ends up importing this file (but like...don't).
46-
from postmark.models.messages import MessageService
47-
48-
self.messages = MessageService(self)
43+
self.outbound = OutboundManager(self)
44+
# self.inbound = InboundManager(self)
4945

5046
async def request(self, method: str, endpoint: str, **kwargs) -> httpx.Response:
5147
"""
@@ -108,3 +104,8 @@ async def put(
108104
self, endpoint: str, json: Optional[Dict[str, Any]] = None
109105
) -> httpx.Response:
110106
return await self.request("PUT", endpoint, json=json)
107+
108+
async def delete(
109+
self, endpoint: str, params: Optional[Dict[str, Any]] = None
110+
) -> httpx.Response:
111+
return await self.request("DELETE", endpoint, params=params)

‎postmark/exceptions.py‎

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Postmark API exceptions."""
22

3-
from typing import Any, Dict, Optional
3+
from typing import Any, Optional
44

55

66
class PostmarkException(Exception):
@@ -15,11 +15,35 @@ def __init__(
1515
super().__init__(message)
1616
self.error_code = error_code
1717
self.http_status = http_status
18-
self.message = message
18+
19+
@property
20+
def message(self) -> str:
21+
"""The error message. Alias for self.args[0]."""
22+
return self.args[0]
23+
24+
25+
class InvalidEmailPayloadException(PostmarkException):
26+
"""
27+
Raised when an email dict or model fails Pydantic validation
28+
before it is ever sent to the API.
29+
30+
Distinct from ValidationException, which maps to HTTP 422 responses
31+
from the Postmark API after a request was made.
32+
"""
33+
34+
def __init__(self, errors: list[dict[str, Any]]):
35+
self.errors = errors
36+
summary = " | ".join(
37+
f"{' -> '.join(str(loc) for loc in e['loc'])}: {e['msg']}" for e in errors
38+
)
39+
super().__init__(f"Invalid email payload: {summary}")
1940

2041

2142
class PostmarkAPIException(PostmarkException):
22-
"""API errors with error codes from Postmark."""
43+
"""
44+
Raised for errors returned by the Postmark API.
45+
Always carries both a Postmark error_code and an http_status.
46+
"""
2347

2448
def __init__(self, message: str, error_code: int, http_status: int):
2549
super().__init__(message, error_code, http_status)
@@ -28,7 +52,6 @@ def __str__(self):
2852
return f"[{self.error_code}] {self.message} (HTTP {self.http_status})"
2953

3054

31-
# Specific exception types for common errors
3255
class InvalidAPIKeyException(PostmarkAPIException):
3356
"""401 - Invalid or missing API key."""
3457

@@ -42,7 +65,7 @@ class InactiveRecipientException(PostmarkAPIException):
4265

4366

4467
class ValidationException(PostmarkAPIException):
45-
"""422 - Invalid request parameters."""
68+
"""422 - Invalid request parameters (rejected by Postmark API)."""
4669

4770
pass
4871

@@ -54,46 +77,47 @@ class RateLimitException(PostmarkAPIException):
5477

5578

5679
class ServerException(PostmarkAPIException):
57-
"""500/503 - Server errors."""
80+
"""500 / 503 - Server-side error."""
5881

5982
pass
6083

6184

6285
class TimeoutException(PostmarkException):
63-
"""Request timeout."""
86+
"""Raised when a request to the Postmark API times out."""
6487

6588
pass
6689

6790

68-
# Error code mapping
69-
ERROR_CODE_MAPPING = {
91+
# Postmark API error codes (from response body), not HTTP status codes.
92+
# See: https://postmarkapp.com/developer/api/overview#error-codes
93+
_ERROR_CODE_MAP: dict[int, type[PostmarkAPIException]] = {
7094
10: InvalidAPIKeyException,
71-
406: InactiveRecipientException,
7295
300: ValidationException,
7396
405: ValidationException, # Not allowed to send
97+
406: InactiveRecipientException,
7498
701: ValidationException, # Illegal attachment type
75-
429: RateLimitException,
76-
500: ServerException,
77-
503: ServerException,
7899
}
79100

80101

81102
def get_exception_class(
82103
error_code: int, http_status: int
83104
) -> type[PostmarkAPIException]:
84-
"""Get the appropriate exception class for an error code."""
85-
# Check specific error codes first
86-
if error_code in ERROR_CODE_MAPPING:
87-
return ERROR_CODE_MAPPING[error_code]
105+
"""
106+
Resolve the most specific exception class for a failed API response.
107+
108+
Checks Postmark error codes first (more specific), then falls back
109+
to HTTP status codes, and finally to the generic PostmarkAPIException.
110+
"""
111+
if error_code in _ERROR_CODE_MAP:
112+
return _ERROR_CODE_MAP[error_code]
88113

89-
# Fall back to HTTP status
90114
if http_status == 401:
91115
return InvalidAPIKeyException
92-
elif http_status == 422:
116+
if http_status == 422:
93117
return ValidationException
94-
elif http_status == 429:
118+
if http_status == 429:
95119
return RateLimitException
96-
elif http_status in (500, 503):
120+
if http_status in (500, 503):
97121
return ServerException
98122

99123
return PostmarkAPIException

‎postmark/models/__init__.py‎

Lines changed: 0 additions & 19 deletions
This file was deleted.

‎postmark/models/messages.py‎

Lines changed: 0 additions & 38 deletions
This file was deleted.

‎postmark/models/messages/__init__.py‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,23 @@
88
Header,
99
EmailAddress,
1010
)
11-
from .manager import MessageService, OutboundManager
11+
from .manager import OutboundManager # InboundManager
1212

1313
__all__ = [
14+
# Schemas
1415
"Email",
1516
"SendResponse",
1617
"Outbound",
1718
"OutboundMessageDetails",
18-
"MessageStatus",
1919
"Attachment",
2020
"Header",
21-
"MessageService",
21+
"EmailAddress",
22+
# Enums
23+
"MessageStatus",
24+
"TrackLinksOption",
25+
"MessageEventType",
26+
"Platform",
27+
# Managers
2228
"OutboundManager",
29+
# "InboundManager,",
2330
]

0 commit comments

Comments
 (0)