Skip to content

Commit 5298da5

Browse files
andrii-balitskyiseambotrazor-x
authored
feat: introduce SeamHttpUnauthorizedError and SeamHttpInvalidInputError (#99)
* Improve http error handling, add invalid input and unauthorized seam errors * Export SeamHttpUnauthorizedError and SeamHttpInvalidInputError * Test new error handling * ci: Format code * Lint fixes * Remove 200 status check from _handle_error_response --------- Co-authored-by: Seam Bot <devops@getseam.com> Co-authored-by: Evan Sosenko <evan@getseam.com>
1 parent d348840 commit 5298da5

6 files changed

Lines changed: 100 additions & 17 deletions

File tree

seam/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from .auth import SeamInvalidTokenError
88
from .exceptions import (
99
SeamHttpApiError,
10+
SeamHttpUnauthorizedError,
11+
SeamHttpInvalidInputError,
1012
SeamActionAttemptError,
1113
SeamActionAttemptFailedError,
1214
SeamActionAttemptTimeoutError,

seam/client.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
from importlib.metadata import version
55

66
from .constants import LTS_VERSION
7-
from .exceptions import SeamHttpApiError
7+
from .exceptions import (
8+
SeamHttpApiError,
9+
SeamHttpInvalidInputError,
10+
SeamHttpUnauthorizedError,
11+
)
812
from .models import AbstractSeamHttpClient
913

1014
SDK_HEADERS = {
@@ -30,10 +34,33 @@ def request(self, method, url, *args, **kwargs):
3034
return self._handle_response(response)
3135

3236
def _handle_response(self, response: requests.Response):
33-
if response.status_code != 200:
34-
raise SeamHttpApiError(response)
37+
if not 200 <= response.status_code < 300:
38+
self._handle_error_response(response)
3539

36-
if "application/json" in response.headers["content-type"]:
40+
if "application/json" in response.headers.get("content-type", ""):
3741
return response.json()
3842

3943
return response.text
44+
45+
def _handle_error_response(self, response: requests.Response):
46+
status_code = response.status_code
47+
request_id = response.headers.get("seam-request-id")
48+
49+
if status_code == 401:
50+
raise SeamHttpUnauthorizedError(request_id)
51+
52+
error = response.json().get("error", {})
53+
error_type = error.get("type", "unknown_error")
54+
error_message = error.get("message", "Unknown error")
55+
error_data = error.get("data", None)
56+
57+
error_details = {
58+
"type": error_type,
59+
"message": error_message,
60+
"data": error_data,
61+
}
62+
63+
if error_type == "invalid_input":
64+
raise SeamHttpInvalidInputError(error_details, status_code, request_id)
65+
66+
raise SeamHttpApiError(error_details, status_code, request_id)

seam/exceptions.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,31 @@
1+
from typing import Any, Dict
12
from niquests import Response
23
from .routes.models import ActionAttempt
34

45

56
# HTTP
67
class SeamHttpApiError(Exception):
7-
def __init__(
8-
self,
9-
response: Response,
10-
):
11-
self.status_code = response.status_code
12-
self.request_id = response.headers.get("seam-request-id", None)
13-
14-
self.metadata = None
15-
if "application/json" in response.headers["content-type"]:
16-
parsed_response = response.json()
17-
self.metadata = parsed_response.get("error", None)
8+
def __init__(self, error: Dict[str, Any], status_code: int, request_id: str):
9+
super().__init__(error["message"])
10+
self.code = error["type"]
11+
self.status_code = status_code
12+
self.request_id = request_id
13+
self.data = error.get("data")
1814

15+
16+
class SeamHttpUnauthorizedError(SeamHttpApiError):
17+
def __init__(self, request_id: str):
1918
super().__init__(
20-
f"SeamApiException: status={self.status_code}, request_id={self.request_id}, metadata={self.metadata}"
19+
{"type": "unauthorized", "message": "Unauthorized"}, 401, request_id
2120
)
2221

2322

23+
class SeamHttpInvalidInputError(SeamHttpApiError):
24+
def __init__(self, error: Dict[str, Any], status_code: int, request_id: str):
25+
super().__init__(error, status_code, request_id)
26+
self.code = "invalid_input"
27+
28+
2429
# Action Attempt
2530
class SeamActionAttemptError(Exception):
2631
def __init__(self, message: str, action_attempt: ActionAttempt):

seam/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ def request(self, method: str, url: str, *args, **kwargs):
1919
def _handle_response(self, response: requests.Response):
2020
raise NotImplementedError
2121

22+
@abc.abstractmethod
23+
def _handle_error_response(self, response: requests.Response):
24+
raise NotImplementedError
25+
2226

2327
class AbstractSeam(AbstractRoutes):
2428
lts_version: str

seam/seam.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from typing import Any, Optional, Union, Dict
2-
import niquests as requests
32
from typing_extensions import Self
43

54
from .constants import LTS_VERSION

test/test_client.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import pytest
2+
from seam import Seam
3+
from seam.exceptions import (
4+
SeamHttpApiError,
5+
SeamHttpInvalidInputError,
6+
SeamHttpUnauthorizedError,
7+
)
8+
9+
10+
def test_seam_http_throws_unauthorized_error(server):
11+
endpoint, _ = server
12+
13+
seam = Seam(api_key="seam_invalid_api_key", endpoint=endpoint)
14+
15+
with pytest.raises(SeamHttpUnauthorizedError) as exc_info:
16+
seam.devices.list()
17+
err = exc_info.value
18+
assert err.status_code == 401
19+
assert err.code == "unauthorized"
20+
assert err.request_id.startswith("request")
21+
22+
23+
def test_seam_http_throws_api_error_on_standard_error_response(server):
24+
endpoint, seed = server
25+
26+
seam = Seam(api_key=seed["seam_apikey1_token"], endpoint=endpoint)
27+
28+
with pytest.raises(SeamHttpApiError) as exc_info:
29+
seam.devices.get(device_id="unknown-device")
30+
err = exc_info.value
31+
assert err.status_code == 404
32+
assert err.code == "device_not_found"
33+
assert err.request_id.startswith("request")
34+
35+
36+
def test_seam_http_throws_invalid_input_error(server):
37+
endpoint, seed = server
38+
39+
seam = Seam(api_key=seed["seam_apikey1_token"], endpoint=endpoint)
40+
41+
with pytest.raises(SeamHttpInvalidInputError) as exc_info:
42+
seam.devices.get(device_id=4242)
43+
err = exc_info.value
44+
assert err.status_code == 400
45+
assert err.code == "invalid_input"
46+
assert err.request_id.startswith("request")

0 commit comments

Comments
 (0)