Skip to content

Commit dd54232

Browse files
committed
re-use api error handling
1 parent b97ac59 commit dd54232

File tree

7 files changed

+62
-93
lines changed

7 files changed

+62
-93
lines changed

src/contiguity/_client.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
11
from __future__ import annotations
22

3+
from http import HTTPStatus
4+
35
from httpx import AsyncClient as HttpxAsyncClient
46
from httpx import Client as HttpxClient
7+
from httpx import Response
58

69
from ._auth import get_contiguity_token
10+
from ._response import ErrorResponse, decode_response
711

812

9-
class ApiError(Exception):
13+
class ContiguityApiError(Exception):
1014
pass
1115

1216

13-
class ApiClient(HttpxClient):
17+
class BaseApiClient:
18+
def handle_error(self, response: Response, /, *, fail_message: str = "api request failed") -> None:
19+
if not HTTPStatus.OK <= response.status_code <= HTTPStatus.IM_USED:
20+
data = decode_response(response.content, type=ErrorResponse)
21+
msg = f"{fail_message}. {response.status_code} {data.error}"
22+
raise ContiguityApiError(msg)
23+
24+
25+
class ApiClient(HttpxClient, BaseApiClient):
1426
def __init__(
1527
self: ApiClient,
1628
*,
@@ -30,7 +42,7 @@ def __init__(
3042
)
3143

3244

33-
class AsyncApiClient(HttpxAsyncClient):
45+
class AsyncApiClient(HttpxAsyncClient, BaseApiClient):
3446
def __init__(
3547
self: AsyncApiClient,
3648
*,

src/contiguity/_instant_messaging.py

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22

33
import logging
44
from abc import ABC, abstractmethod
5-
from http import HTTPStatus
65
from typing import TYPE_CHECKING, Generic, Literal, TypeVar
76

87
from ._product import BaseProduct
9-
from ._response import BaseResponse, ErrorResponse, decode_response
8+
from ._response import BaseResponse, decode_response
109

1110
if TYPE_CHECKING:
1211
from collections.abc import Sequence
@@ -57,11 +56,7 @@ def send( # noqa: PLR0913
5756
json={k: v for k, v in payload.items() if v is not None},
5857
)
5958

60-
if response.status_code != HTTPStatus.OK:
61-
data = decode_response(response.content, type=ErrorResponse)
62-
msg = f"failed to send instant message. {response.status_code} {data.error}"
63-
raise ValueError(msg)
64-
59+
self._client.handle_error(response, fail_message="failed to send instant message")
6560
data = decode_response(response.content, type=IMSendResponse)
6661
logger.debug("successfully sent %s message to %r", self._api_path[1:], to)
6762
return data
@@ -78,11 +73,7 @@ def _typing(self, *, to: str, action: Literal["start", "stop"], from_: str | Non
7873
json={k: v for k, v in payload.items() if v is not None},
7974
)
8075

81-
if response.status_code != HTTPStatus.OK:
82-
data = decode_response(response.content, type=ErrorResponse)
83-
msg = f"failed to {action} {self._api_path[1:]} typing indicator. {response.status_code} {data.error}"
84-
raise ValueError(msg)
85-
76+
self._client.handle_error(response, fail_message=f"failed to {action} {self._api_path[1:]} typing indicator")
8677
data = decode_response(response.content, type=IMTypingResponse)
8778
logger.debug("successfully %s %s typing indicator for %r", action, self._api_path[1:], to)
8879
return data
@@ -113,11 +104,7 @@ def _reactions(
113104
json={k: v for k, v in payload.items() if v is not None},
114105
)
115106

116-
if response.status_code != HTTPStatus.OK:
117-
data = decode_response(response.content, type=ErrorResponse)
118-
msg = f"failed to {action} {self._api_path[1:]} reaction. {response.status_code} {data.error}"
119-
raise ValueError(msg)
120-
107+
self._client.handle_error(response, fail_message=f"failed to {action} {self._api_path[1:]} reaction")
121108
data = decode_response(response.content, type=IMReactionResponse)
122109
logger.debug("successfully %s %s reaction for %r", action, self._api_path[1:], to)
123110
return data

src/contiguity/_response.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
from collections.abc import Sequence
12
from http import HTTPStatus
2-
from typing import Generic, TypeVar
3+
from typing import Generic, TypeVar, Union
34

45
import msgspec
56

6-
T = TypeVar("T", bound=msgspec.Struct)
7+
T = TypeVar("T", bound=Union[msgspec.Struct, Sequence[msgspec.Struct]])
78

89

910
class ResponseMetadata(msgspec.Struct):

src/contiguity/domains.py

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import logging
22
from collections.abc import Sequence
3-
from http import HTTPStatus
43

54
import msgspec
65

76
from ._product import BaseProduct
8-
from ._response import BaseResponse, ErrorResponse, decode_response
7+
from ._response import BaseResponse, decode_response
98

109
logger = logging.getLogger(__name__)
1110

@@ -59,43 +58,24 @@ def register(
5958
},
6059
)
6160

62-
if response.status_code != HTTPStatus.OK:
63-
data = decode_response(response.content, type=ErrorResponse)
64-
msg = f"failed to register domain. {response.status_code} {data.error}"
65-
raise ValueError(msg)
66-
61+
self._client.handle_error(response, fail_message="failed to register domain")
6762
data = decode_response(response.content, type=PartialDomain)
6863
logger.debug("successfully registered domain %r", domain)
6964
return data
7065

7166
def list(self) -> list[PartialDomain]:
7267
response = self._client.get("/domains")
73-
74-
if response.status_code != HTTPStatus.OK:
75-
data = decode_response(response.content, type=ErrorResponse)
76-
msg = f"failed to list domains. {response.status_code} {data.error}"
77-
raise ValueError(msg)
78-
68+
self._client.handle_error(response, fail_message="failed to list domains")
7969
return decode_response(response.content, type=list[PartialDomain])
8070

8171
def get(self, domain: str, /) -> Domain:
8272
response = self._client.get(f"/domains/{domain}")
83-
84-
if response.status_code != HTTPStatus.OK:
85-
data = decode_response(response.content, type=ErrorResponse)
86-
msg = f"failed to get domain. {response.status_code} {data.error}"
87-
raise ValueError(msg)
88-
73+
self._client.handle_error(response, fail_message="failed to get domain")
8974
return decode_response(response.content, type=Domain)
9075

9176
def delete(self, domain: str, /) -> DeleteDomainResponse:
9277
response = self._client.delete(f"/domains/{domain}")
93-
94-
if response.status_code != HTTPStatus.OK:
95-
data = decode_response(response.content, type=ErrorResponse)
96-
msg = f"failed to delete domain. {response.status_code} {data.error}"
97-
raise ValueError(msg)
98-
78+
self._client.handle_error(response, fail_message="failed to delete domain")
9979
data = decode_response(response.content, type=DeleteDomainResponse)
10080
logger.debug("successfully deleted domain %r", domain)
10181
return data

src/contiguity/email.py

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
from __future__ import annotations
22

33
import logging
4-
from http import HTTPStatus
5-
from typing import overload
4+
from typing import TYPE_CHECKING, overload
65

76
from ._product import BaseProduct
8-
from ._response import BaseResponse, ErrorResponse, decode_response
7+
from ._response import BaseResponse, decode_response
8+
9+
if TYPE_CHECKING:
10+
from collections.abc import Mapping, Sequence
911

1012
logger = logging.getLogger(__name__)
1113

1214

1315
class EmailResponse(BaseResponse):
14-
message: str
16+
email_id: str
1517

1618

1719
class Email(BaseProduct):
@@ -45,10 +47,12 @@ def email( # noqa: PLR0913
4547
to: str,
4648
from_: str,
4749
subject: str,
48-
reply_to: str = "",
49-
cc: str = "",
50-
text: str | None = None,
51-
html: str | None = None,
50+
body_text: str | None = None,
51+
body_html: str | None = None,
52+
reply_to: str | None = None,
53+
cc: str | Sequence[str] | None = None,
54+
bcc: str | Sequence[str] | None = None,
55+
headers: Mapping[str, str] | None = None,
5256
) -> EmailResponse:
5357
"""
5458
Send an email.
@@ -67,27 +71,30 @@ def email( # noqa: PLR0913
6771
Raises:
6872
ValueError: Raises an error if required fields are missing or sending the email fails.
6973
"""
74+
if not body_text and not body_html:
75+
msg = "either text or html body must be provided"
76+
raise ValueError(msg)
77+
7078
email_payload = {
7179
"to": to,
7280
"from": from_,
7381
"subject": subject,
74-
"body": html or text,
75-
"contentType": "html" if html else "text",
82+
"body": {
83+
"text": body_text,
84+
"html": body_html,
85+
},
86+
"reply_to": reply_to,
87+
"cc": cc,
88+
"bcc": bcc,
89+
"headers": headers,
7690
}
7791

78-
if reply_to:
79-
email_payload["replyTo"] = reply_to
80-
81-
if cc:
82-
email_payload["cc"] = cc
83-
84-
response = self._client.post("/send/email", json=email_payload)
85-
86-
if response.status_code != HTTPStatus.OK:
87-
data = decode_response(response.content, type=ErrorResponse)
88-
msg = f"failed to send email. {response.status_code} {data.error}"
89-
raise ValueError(msg)
92+
response = self._client.post(
93+
"/send/email",
94+
json={k: v for k, v in email_payload.items() if v},
95+
)
9096

97+
self._client.handle_error(response, fail_message="failed to send email")
9198
data = decode_response(response.content, type=EmailResponse)
9299
logger.debug("successfully sent email to %r", to)
93100
return data

src/contiguity/otp.py

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22

33
import logging
44
from enum import Enum
5-
from http import HTTPStatus
65

76
import phonenumbers
87

98
from ._product import BaseProduct
10-
from ._response import BaseResponse, ErrorResponse, decode_response
9+
from ._response import BaseResponse, decode_response
1110

1211
logger = logging.getLogger(__name__)
1312

@@ -83,11 +82,7 @@ def send(
8382
},
8483
)
8584

86-
if response.status_code != HTTPStatus.OK:
87-
data = decode_response(response.content, type=ErrorResponse)
88-
msg = f"failed to send OTP. {response.status_code} {data.error}"
89-
raise ValueError(msg)
90-
85+
self._client.handle_error(response, fail_message="failed to send OTP")
9186
data = decode_response(response.content, type=OTPSendResponse)
9287
logger.debug("successfully sent OTP %r to %r", data.otp_id, to)
9388
return data
@@ -100,11 +95,7 @@ def resend(self, otp_id: str, /) -> OTPResendResponse:
10095
},
10196
)
10297

103-
if response.status_code != HTTPStatus.OK:
104-
data = decode_response(response.content, type=ErrorResponse)
105-
msg = f"failed to resend OTP. {response.status_code} {data.error}"
106-
raise ValueError(msg)
107-
98+
self._client.handle_error(response, fail_message="failed to resend OTP")
10899
data = decode_response(response.content, type=OTPResendResponse)
109100
logger.debug("successfully resent OTP %r with status: %r", otp_id, data.resent)
110101
return data
@@ -118,11 +109,7 @@ def verify(self, otp: int | str, /, *, otp_id: str) -> OTPVerifyResponse:
118109
},
119110
)
120111

121-
if response.status_code != HTTPStatus.OK:
122-
data = decode_response(response.content, type=ErrorResponse)
123-
msg = f"failed to verify OTP. {response.status_code} {data.error}"
124-
raise ValueError(msg)
125-
112+
self._client.handle_error(response, fail_message="failed to verify OTP")
126113
data = decode_response(response.content, type=OTPVerifyResponse)
127114
logger.debug("successfully verified OTP %r with status: %r", otp_id, data.verified)
128115
return data

src/contiguity/text.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import logging
2-
from http import HTTPStatus
32

43
import phonenumbers
54

65
from ._product import BaseProduct
7-
from ._response import BaseResponse, ErrorResponse, decode_response
6+
from ._response import BaseResponse, decode_response
87

98
logger = logging.getLogger(__name__)
109

@@ -42,11 +41,7 @@ def send(self, *, to: str, message: str) -> TextResponse:
4241
},
4342
)
4443

45-
if response.status_code != HTTPStatus.OK:
46-
data = decode_response(response.content, type=ErrorResponse)
47-
msg = f"failed to send message. {response.status_code} {data.error}"
48-
raise ValueError(msg)
49-
44+
self._client.handle_error(response, fail_message="failed to send text message")
5045
data = decode_response(response.content, type=TextResponse)
5146
logger.debug("successfully sent text to %r", to)
5247
return data

0 commit comments

Comments
 (0)