Skip to content

Commit c92c20c

Browse files
committed
new error factory
1 parent 25b2c7c commit c92c20c

File tree

2 files changed

+79
-11
lines changed

2 files changed

+79
-11
lines changed

packages/common-library/src/common_library/errors_classes.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,30 @@ def error_context(self) -> dict[str, Any]:
5252
def error_code(self) -> str:
5353
assert isinstance(self, Exception), "subclass must be exception" # nosec
5454
return create_error_code(self)
55+
56+
57+
class NotFoundError(OsparcErrorMixin):
58+
msg_template = "{resource} not found: id='{resource_id}'"
59+
60+
61+
class ForbiddenError(OsparcErrorMixin):
62+
msg_template = "Access to {resource} is forbidden: id='{resource_id}'"
63+
64+
65+
def make_resource_error(
66+
resource: str,
67+
error_cls: type[OsparcErrorMixin],
68+
base_exception: type[Exception] = Exception,
69+
) -> type[OsparcErrorMixin]:
70+
class _ResourceError(error_cls, base_exception):
71+
def __init__(self, **ctx: Any):
72+
ctx.setdefault("resource", resource)
73+
# guesses identifer e.g. project_id, user_id
74+
if resource_id := ctx.get(f"{resource.lower()}_id"):
75+
ctx.setdefault("resource_id", resource_id)
76+
77+
super().__init__(**ctx)
78+
79+
resource_class_name = "".join(word.capitalize() for word in resource.split("_"))
80+
_ResourceError.__name__ = f"{resource_class_name}{error_cls.__name__}"
81+
return _ResourceError

packages/common-library/tests/test_errors_classes.py

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,24 @@
99
from typing import Any
1010

1111
import pytest
12-
from common_library.errors_classes import OsparcErrorMixin
12+
from common_library.errors_classes import (
13+
ForbiddenError,
14+
NotFoundError,
15+
OsparcErrorMixin,
16+
make_resource_error,
17+
)
1318

1419

1520
def test_get_full_class_name():
16-
class A(OsparcErrorMixin):
17-
...
21+
class A(OsparcErrorMixin): ...
1822

19-
class B1(A):
20-
...
23+
class B1(A): ...
2124

22-
class B2(A):
23-
...
25+
class B2(A): ...
2426

25-
class C(B2):
26-
...
27+
class C(B2): ...
2728

28-
class B12(B1, ValueError):
29-
...
29+
class B12(B1, ValueError): ...
3030

3131
assert B1._get_full_class_name() == "A.B1"
3232
assert C._get_full_class_name() == "A.B2.C"
@@ -159,3 +159,44 @@ class MyError(OsparcErrorMixin, ValueError):
159159
"message": "42 and 'missing=?'",
160160
"value": 42,
161161
}
162+
163+
164+
def test_resource_error_factory():
165+
ProjectNotFoundError = make_resource_error("project", NotFoundError)
166+
167+
error_1 = ProjectNotFoundError(resource_id="abc123")
168+
assert "resource_id" in error_1.error_context()
169+
assert error_1.resource_id in error_1.message # type: ignore
170+
171+
172+
def test_resource_error_factory_auto_detect_resource_id():
173+
ProjectForbiddenError = make_resource_error("project", ForbiddenError)
174+
error_2 = ProjectForbiddenError(project_id="abc123", other_id="foo")
175+
assert (
176+
error_2.resource_id == error_2.project_id # type: ignore
177+
), "auto-detects project ids as resourceid"
178+
assert error_2.other_id # type: ignore
179+
assert error_2.code == "ForbiddenError.ProjectForbiddenError"
180+
181+
assert error_2.error_context() == {
182+
"project_id": "abc123",
183+
"other_id": "foo",
184+
"resource": "project",
185+
"resource_id": "abc123",
186+
"message": "Access to project is forbidden: id='abc123'",
187+
"code": "ForbiddenError.ProjectForbiddenError",
188+
}
189+
190+
191+
def test_resource_error_factory_different_base_exception():
192+
193+
class MyBaseError(Exception): ...
194+
195+
OtherProjectForbiddenError = make_resource_error(
196+
"other_project", ForbiddenError, MyBaseError
197+
)
198+
199+
assert issubclass(OtherProjectForbiddenError, MyBaseError)
200+
201+
error_3 = OtherProjectForbiddenError(project_id="abc123")
202+
assert error_3.code == "MyBaseError.ForbiddenError.OtherProjectForbiddenError"

0 commit comments

Comments
 (0)