prepare results wrapping for error handling architecture
This commit is contained in:
parent
8d6a25aaf7
commit
adbc894899
@ -1,13 +1,19 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Any, Final
|
import typing as t
|
||||||
|
from collections.abc import Callable
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Any, Final
|
||||||
|
|
||||||
from delta_barth.constants import DEFAULT_API_ERR_CODE, DEFAULT_INTERNAL_ERR_CODE
|
from delta_barth.constants import DEFAULT_API_ERR_CODE, DEFAULT_INTERNAL_ERR_CODE
|
||||||
from delta_barth.types import DataPipeStates, Status
|
from delta_barth.types import DataPipeStates, Status
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if t.TYPE_CHECKING:
|
||||||
from delta_barth.types import DelBarApiError, StatusDescription
|
from delta_barth.types import DelBarApiError, StatusDescription
|
||||||
|
|
||||||
|
P = t.ParamSpec("P")
|
||||||
|
T = t.TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
class UnspecifiedRequestType(Exception):
|
class UnspecifiedRequestType(Exception):
|
||||||
"""exception raised if for a given API endpoint a not defined operation is requested"""
|
"""exception raised if for a given API endpoint a not defined operation is requested"""
|
||||||
@ -21,6 +27,12 @@ class FeaturesMissingError(Exception):
|
|||||||
"""exception raised if needed features are missing"""
|
"""exception raised if needed features are missing"""
|
||||||
|
|
||||||
|
|
||||||
|
# ** Exceptions for result wrappers
|
||||||
|
class WAccessResultDespiteError(Exception):
|
||||||
|
"""wrapped results exception: raised if result is accessed, even though
|
||||||
|
there was an error in the underlying procedure"""
|
||||||
|
|
||||||
|
|
||||||
# ** Exceptions for unwrap of error value
|
# ** Exceptions for unwrap of error value
|
||||||
class UDataProcessingError(Exception):
|
class UDataProcessingError(Exception):
|
||||||
"""unwrap exception: all data related errors (e.g. wrong format, non-sufficient quality)"""
|
"""unwrap exception: all data related errors (e.g. wrong format, non-sufficient quality)"""
|
||||||
@ -88,8 +100,17 @@ class StatusHandler:
|
|||||||
|
|
||||||
self._pipe_states = DataPipeStates(**parsed_errors)
|
self._pipe_states = DataPipeStates(**parsed_errors)
|
||||||
|
|
||||||
def error(
|
def exception_to_error(
|
||||||
self,
|
self,
|
||||||
|
exception: Exception,
|
||||||
|
code: int = DEFAULT_INTERNAL_ERR_CODE,
|
||||||
|
) -> Status:
|
||||||
|
description = exception.__class__.__name__
|
||||||
|
message = str(exception)
|
||||||
|
return self.error(description, message, code)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def error(
|
||||||
description: str,
|
description: str,
|
||||||
message: str = "",
|
message: str = "",
|
||||||
code: int = DEFAULT_INTERNAL_ERR_CODE,
|
code: int = DEFAULT_INTERNAL_ERR_CODE,
|
||||||
@ -108,8 +129,8 @@ class StatusHandler:
|
|||||||
message=message,
|
message=message,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def api_error(
|
def api_error(
|
||||||
self,
|
|
||||||
error: DelBarApiError,
|
error: DelBarApiError,
|
||||||
) -> Status:
|
) -> Status:
|
||||||
description = "Es ist ein Fehler bei der Kommunikation mit dem API-Server aufgetreten"
|
description = "Es ist ein Fehler bei der Kommunikation mit dem API-Server aufgetreten"
|
||||||
@ -148,3 +169,68 @@ class StatusHandler:
|
|||||||
|
|
||||||
|
|
||||||
STATUS_HANDLER: Final[StatusHandler] = StatusHandler()
|
STATUS_HANDLER: Final[StatusHandler] = StatusHandler()
|
||||||
|
|
||||||
|
|
||||||
|
# ** result wrapping
|
||||||
|
class NotSet:
|
||||||
|
__slots__ = tuple()
|
||||||
|
|
||||||
|
def __init__(self) -> None: ...
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return ">Not set<"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.__class__.__name__}()"
|
||||||
|
|
||||||
|
|
||||||
|
class ResultWrapper(t.Generic[T]):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
result: T | NotSet,
|
||||||
|
exception: Exception | None,
|
||||||
|
code_on_error: int,
|
||||||
|
) -> None:
|
||||||
|
self._result = result
|
||||||
|
status: Status = STATUS_HANDLER.SUCCESS
|
||||||
|
if exception is not None:
|
||||||
|
status = STATUS_HANDLER.exception_to_error(exception, code=code_on_error)
|
||||||
|
self.status = status
|
||||||
|
|
||||||
|
@property
|
||||||
|
def result(self) -> T:
|
||||||
|
if isinstance(self._result, NotSet):
|
||||||
|
raise WAccessResultDespiteError(
|
||||||
|
"Can not access result because an error occurred during procedure"
|
||||||
|
)
|
||||||
|
return self._result
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"Status(result: {self._result}, status: {self.status})"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return self.__str__()
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_result(
|
||||||
|
code_on_error: int = DEFAULT_API_ERR_CODE,
|
||||||
|
) -> Callable[[Callable[P, T]], Callable[P, ResultWrapper[T]]]:
|
||||||
|
def wrap_result(func: Callable[P, T]) -> Callable[P, ResultWrapper[T]]:
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args: P.args, **kwargs: P.kwargs) -> ResultWrapper[T]:
|
||||||
|
status: ResultWrapper[T]
|
||||||
|
try:
|
||||||
|
res = func(*args, **kwargs)
|
||||||
|
status = ResultWrapper(
|
||||||
|
result=res, exception=None, code_on_error=code_on_error
|
||||||
|
)
|
||||||
|
except Exception as err:
|
||||||
|
status = ResultWrapper(
|
||||||
|
result=NotSet(), exception=err, code_on_error=code_on_error
|
||||||
|
)
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return wrap_result
|
||||||
|
|||||||
@ -1,14 +1,20 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import enum
|
import enum
|
||||||
|
import typing as t
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import NotRequired, TypeAlias, TypedDict
|
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from pydantic import BaseModel, ConfigDict, SkipValidation
|
from pydantic import BaseModel, ConfigDict, SkipValidation
|
||||||
|
|
||||||
# ** Pipeline state management
|
# ** Pipeline state management
|
||||||
StatusDescription: TypeAlias = tuple[str, int, str]
|
StatusDescription: t.TypeAlias = tuple[str, int, str]
|
||||||
|
|
||||||
|
|
||||||
|
class IError(t.Protocol):
|
||||||
|
code: int
|
||||||
|
description: str
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
class Status(BaseModel):
|
class Status(BaseModel):
|
||||||
@ -59,12 +65,12 @@ class HttpRequestTypes(enum.StrEnum):
|
|||||||
DELETE = enum.auto()
|
DELETE = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
HttpContentHeaders = TypedDict(
|
HttpContentHeaders = t.TypedDict(
|
||||||
"HttpContentHeaders",
|
"HttpContentHeaders",
|
||||||
{
|
{
|
||||||
"Content-type": str,
|
"Content-type": str,
|
||||||
"Accept": str,
|
"Accept": str,
|
||||||
"DelecoToken": NotRequired[str],
|
"DelecoToken": t.NotRequired[str],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -76,7 +76,7 @@ def test_status_handler_api_error():
|
|||||||
assert new_err.api_server_error == api_err
|
assert new_err.api_server_error == api_err
|
||||||
|
|
||||||
|
|
||||||
def test_status_handler_raising():
|
def test_status_handler_unwrap():
|
||||||
status_hdlr = errors.StatusHandler()
|
status_hdlr = errors.StatusHandler()
|
||||||
|
|
||||||
# success: should not raise
|
# success: should not raise
|
||||||
@ -121,3 +121,47 @@ def test_status_handler_raising():
|
|||||||
assert description in descr
|
assert description in descr
|
||||||
assert msg in descr
|
assert msg in descr
|
||||||
raise err
|
raise err
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_handler_exception_parsing():
|
||||||
|
status_hdlr = errors.StatusHandler()
|
||||||
|
test_message = "Test Exception"
|
||||||
|
test_code = 110
|
||||||
|
exception = ValueError(test_message)
|
||||||
|
status = status_hdlr.exception_to_error(exception, test_code)
|
||||||
|
assert status.code == test_code
|
||||||
|
assert status.description == exception.__class__.__name__
|
||||||
|
assert status.message == test_message
|
||||||
|
|
||||||
|
|
||||||
|
def test_not_set():
|
||||||
|
not_set = errors.NotSet()
|
||||||
|
# using slots, dynamic attribute generation should not be possible
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
not_set.test = "try to set value" # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def test_result_wrapper_class():
|
||||||
|
test_result = 10
|
||||||
|
error_code = 146
|
||||||
|
twrapper: errors.ResultWrapper[int] = errors.ResultWrapper(
|
||||||
|
result=test_result,
|
||||||
|
exception=None,
|
||||||
|
code_on_error=error_code,
|
||||||
|
)
|
||||||
|
assert twrapper.status == errors.STATUS_HANDLER.SUCCESS
|
||||||
|
assert twrapper.result == test_result
|
||||||
|
assert twrapper.status.code != error_code
|
||||||
|
# test for no result
|
||||||
|
test_result = errors.NotSet()
|
||||||
|
test_message = "Test of error message"
|
||||||
|
exception = ValueError(test_message)
|
||||||
|
twrapper: errors.ResultWrapper[int] = errors.ResultWrapper(
|
||||||
|
result=test_result,
|
||||||
|
exception=exception,
|
||||||
|
code_on_error=error_code,
|
||||||
|
)
|
||||||
|
assert twrapper.status != errors.STATUS_HANDLER.SUCCESS
|
||||||
|
assert twrapper.status.code == error_code
|
||||||
|
with pytest.raises(errors.WAccessResultDespiteError):
|
||||||
|
twrapper.result
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user