prepare results wrapping for error handling architecture

This commit is contained in:
Florian Förster 2025-03-07 16:03:43 +01:00
parent 8d6a25aaf7
commit adbc894899
3 changed files with 145 additions and 9 deletions

View File

@ -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

View File

@ -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],
}, },
) )

View File

@ -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