Skip to content

Commit 2a4df33

Browse files
authored
Merge pull request #25 from xJeffx23/feature/not-found-error
feat(errors): add NotFoundError with resource_type and resource_id
2 parents d9eb3d8 + f8d35dd commit 2a4df33

2 files changed

Lines changed: 97 additions & 1 deletion

File tree

src/shade/errors.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import json
34
from typing import Optional
45

56

@@ -32,7 +33,44 @@ class InvalidRequestError(ShadeError):
3233

3334

3435
class NotFoundError(ShadeError):
35-
"""Raised when an API resource cannot be found."""
36+
"""Raised on HTTP 404 responses.
37+
38+
Attributes:
39+
resource_type: Kind of resource that was not found (e.g. "payment", "invoice").
40+
resource_id: ID of the missing resource.
41+
"""
42+
43+
def __init__(
44+
self,
45+
message: str,
46+
status_code: Optional[int] = None,
47+
response_body: Optional[str] = None,
48+
resource_type: Optional[str] = None,
49+
resource_id: Optional[str] = None,
50+
) -> None:
51+
super().__init__(message, status_code, response_body)
52+
parsed = _parse_body(response_body)
53+
self.resource_type: Optional[str] = resource_type or parsed.get("resource_type")
54+
self.resource_id: Optional[str] = resource_id or parsed.get("resource_id")
55+
56+
@classmethod
57+
def from_response(
58+
cls,
59+
message: str,
60+
response_body: Optional[str] = None,
61+
) -> "NotFoundError":
62+
"""Construct from a raw 404 response body."""
63+
return cls(message, status_code=404, response_body=response_body)
64+
65+
66+
def _parse_body(response_body: Optional[str]) -> dict:
67+
if not response_body:
68+
return {}
69+
try:
70+
data = json.loads(response_body)
71+
return data if isinstance(data, dict) else {}
72+
except (json.JSONDecodeError, ValueError):
73+
return {}
3674

3775

3876
class NetworkError(ShadeError):

tests/test_errors.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,61 @@ def test_package_root_exports_error_classes():
4949
assert shade.InvalidRequestError is InvalidRequestError
5050
assert shade.NetworkError is NetworkError
5151
assert shade.NotFoundError is NotFoundError
52+
53+
54+
def test_not_found_error_is_shade_error():
55+
error = NotFoundError("not found", status_code=404)
56+
assert isinstance(error, ShadeError)
57+
58+
59+
def test_not_found_error_parses_resource_from_body():
60+
body = '{"resource_type": "payment", "resource_id": "pay_abc123"}'
61+
error = NotFoundError("not found", status_code=404, response_body=body)
62+
63+
assert error.resource_type == "payment"
64+
assert error.resource_id == "pay_abc123"
65+
66+
67+
def test_not_found_error_explicit_attrs_override_body():
68+
body = '{"resource_type": "invoice", "resource_id": "inv_999"}'
69+
error = NotFoundError(
70+
"not found",
71+
status_code=404,
72+
response_body=body,
73+
resource_type="payment",
74+
resource_id="pay_001",
75+
)
76+
77+
assert error.resource_type == "payment"
78+
assert error.resource_id == "pay_001"
79+
80+
81+
def test_not_found_error_none_when_body_missing():
82+
error = NotFoundError("not found", status_code=404)
83+
84+
assert error.resource_type is None
85+
assert error.resource_id is None
86+
87+
88+
def test_not_found_error_none_when_body_lacks_fields():
89+
error = NotFoundError("not found", status_code=404, response_body='{"error":"gone"}')
90+
91+
assert error.resource_type is None
92+
assert error.resource_id is None
93+
94+
95+
def test_not_found_error_from_response_factory():
96+
body = '{"resource_type": "invoice", "resource_id": "inv_456"}'
97+
error = NotFoundError.from_response("invoice not found", response_body=body)
98+
99+
assert error.status_code == 404
100+
assert error.resource_type == "invoice"
101+
assert error.resource_id == "inv_456"
102+
assert isinstance(error, ShadeError)
103+
104+
105+
def test_not_found_error_invalid_json_body():
106+
error = NotFoundError("not found", status_code=404, response_body="not-json")
107+
108+
assert error.resource_type is None
109+
assert error.resource_id is None

0 commit comments

Comments
 (0)