basic structure for generic result pattern
This commit is contained in:
parent
04b17c9e70
commit
3f9d5e3652
205
src/dopt_basics/result_pattern.py
Normal file
205
src/dopt_basics/result_pattern.py
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
import dataclasses as dc
|
||||||
|
import inspect
|
||||||
|
import typing as t
|
||||||
|
from collections.abc import Callable
|
||||||
|
from functools import wraps
|
||||||
|
from logging import Logger
|
||||||
|
|
||||||
|
P = t.ParamSpec("P")
|
||||||
|
T = t.TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
# ** 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
|
||||||
|
class UDataProcessingError(Exception):
|
||||||
|
"""unwrap exception: all data related errors (e.g. wrong format, non-sufficient quality)"""
|
||||||
|
|
||||||
|
|
||||||
|
class UInternalError(Exception):
|
||||||
|
"""unwrap exception: all internal errors; internal, if error is not caused by (external)
|
||||||
|
data related issues"""
|
||||||
|
|
||||||
|
|
||||||
|
class UApiError(Exception):
|
||||||
|
"""unwrap exception: all errors occurred on the server side of Delta Barth's API
|
||||||
|
default case: should not be raised, as this kind of errors should be handled by
|
||||||
|
Delta Barth themselves"""
|
||||||
|
|
||||||
|
|
||||||
|
@dc.dataclass(kw_only=True, slots=True)
|
||||||
|
class Status:
|
||||||
|
code: int
|
||||||
|
description: str
|
||||||
|
message: str
|
||||||
|
ExceptionType: type[Exception] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class StatusHandler:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
logger: Logger | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
self._SUCCESS: t.Final[Status] = Status(
|
||||||
|
code=0, description="SUCCESS", message="operation executed successfully"
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def SUCCESS(self) -> Status:
|
||||||
|
return self._SUCCESS
|
||||||
|
|
||||||
|
def error_to_exception(
|
||||||
|
self,
|
||||||
|
error_state: Status,
|
||||||
|
) -> Exception:
|
||||||
|
if error_state.ExceptionType is None:
|
||||||
|
raise ValueError(
|
||||||
|
"Cannot construct exception from state where no exception is defined"
|
||||||
|
)
|
||||||
|
return error_state.ExceptionType(error_state.message)
|
||||||
|
|
||||||
|
def exception_to_error(
|
||||||
|
self,
|
||||||
|
exception: Exception,
|
||||||
|
code: int,
|
||||||
|
) -> Status:
|
||||||
|
doc_string = inspect.getdoc(exception)
|
||||||
|
description = doc_string if doc_string is not None else ""
|
||||||
|
message = str(exception)
|
||||||
|
return self.error_state(
|
||||||
|
code,
|
||||||
|
description,
|
||||||
|
message,
|
||||||
|
exception=exception,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def error_state(
|
||||||
|
code: int,
|
||||||
|
description: str,
|
||||||
|
message: str,
|
||||||
|
exception: Exception | None,
|
||||||
|
) -> Status:
|
||||||
|
if code == 0:
|
||||||
|
raise ValueError(
|
||||||
|
"Custom error codes must not be zero since this value "
|
||||||
|
"is reserved for successful operations"
|
||||||
|
)
|
||||||
|
elif code < 0:
|
||||||
|
raise ValueError("Custom error codes must not be smaller than zero")
|
||||||
|
|
||||||
|
exception_type: type[Exception] | None = None
|
||||||
|
if exception is not None:
|
||||||
|
exception_type = type(exception)
|
||||||
|
|
||||||
|
return Status(
|
||||||
|
code=code,
|
||||||
|
description=description,
|
||||||
|
message=message,
|
||||||
|
ExceptionType=exception_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
def raise_for_status(
|
||||||
|
self,
|
||||||
|
state: Status,
|
||||||
|
) -> None:
|
||||||
|
if state == self.SUCCESS:
|
||||||
|
if self.logger is not None:
|
||||||
|
self.logger.info(
|
||||||
|
"[STATUS] Raise for status - SUCCESS. all good.", stack_info=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
exc = self.error_to_exception(error_state=state)
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
|
||||||
|
class NotSet:
|
||||||
|
__slots__ = tuple()
|
||||||
|
|
||||||
|
def __init__(self) -> None: ...
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return ">Not set<"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.__class__.__name__}()"
|
||||||
|
|
||||||
|
|
||||||
|
STATUS_HANDLER: t.Final[StatusHandler] = StatusHandler()
|
||||||
|
|
||||||
|
|
||||||
|
class ResultWrapper(t.Generic[T]):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
result: T | NotSet,
|
||||||
|
exception: Exception | None,
|
||||||
|
code_on_error: int,
|
||||||
|
) -> None:
|
||||||
|
assert (isinstance(result, NotSet) and exception is not None) or (
|
||||||
|
not isinstance(result, NotSet) and exception is None
|
||||||
|
), "set >NotSet< without exception or result with exception"
|
||||||
|
|
||||||
|
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 it is not set")
|
||||||
|
return self._result
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"Status(result: {self._result}, status: {self.status})"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return self.__str__()
|
||||||
|
|
||||||
|
def unwrap(self) -> T:
|
||||||
|
STATUS_HANDLER.raise_for_status(self.status)
|
||||||
|
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_result(
|
||||||
|
code_on_error: int,
|
||||||
|
logger: Logger | None = None,
|
||||||
|
) -> 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]:
|
||||||
|
wrapped_result: ResultWrapper[T]
|
||||||
|
try:
|
||||||
|
res = func(*args, **kwargs)
|
||||||
|
wrapped_result = ResultWrapper(
|
||||||
|
result=res, exception=None, code_on_error=code_on_error
|
||||||
|
)
|
||||||
|
except Exception as err:
|
||||||
|
wrapped_result = ResultWrapper(
|
||||||
|
result=NotSet(), exception=err, code_on_error=code_on_error
|
||||||
|
)
|
||||||
|
if logger is not None:
|
||||||
|
logger.info(
|
||||||
|
(
|
||||||
|
"[RESULT-WRAPPER] An exception in routine %s occurred - msg: %s, "
|
||||||
|
"stack trace:"
|
||||||
|
),
|
||||||
|
func.__name__,
|
||||||
|
str(err),
|
||||||
|
stack_info=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return wrapped_result
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return wrap_result
|
||||||
Loading…
x
Reference in New Issue
Block a user