new architecture for error handling

This commit is contained in:
Florian Förster 2025-03-05 12:38:12 +01:00
parent 6b4495eb7a
commit c6739ef28c
7 changed files with 129 additions and 22 deletions

View File

@ -10,7 +10,7 @@ from xgboost import XGBRegressor
from delta_barth.analysis import parse from delta_barth.analysis import parse
from delta_barth.constants import COL_MAP_SALES_PROGNOSIS, FEATURES_SALES_PROGNOSIS from delta_barth.constants import COL_MAP_SALES_PROGNOSIS, FEATURES_SALES_PROGNOSIS
from delta_barth.types import CustomerDataSalesForecast, FcErrorCodes from delta_barth.types import CustomerDataSalesForecast, DataPipelineErrors
if TYPE_CHECKING: if TYPE_CHECKING:
from delta_barth.api.common import SalesPrognosisResponse from delta_barth.api.common import SalesPrognosisResponse
@ -105,7 +105,7 @@ def sales_per_customer(
# check data availability # check data availability
if len(df_cust) < min_num_data_points: if len(df_cust) < min_num_data_points:
return FcErrorCodes.DATA_TOO_FEW_POINTS, None return DataPipelineErrors.DATA_TOO_FEW_POINTS, None
else: else:
# Entwicklung der Umsätze: definierte Zeiträume Monat # Entwicklung der Umsätze: definierte Zeiträume Monat
df_cust["year"] = df_cust["date"].dt.year df_cust["year"] = df_cust["date"].dt.year
@ -144,4 +144,4 @@ def sales_per_customer(
test = test.reset_index(drop=True) test = test.reset_index(drop=True)
# umsetzung, prognose # umsetzung, prognose
return FcErrorCodes.SUCCESS, test return DataPipelineErrors.SUCCESS, test

View File

@ -5,10 +5,10 @@ from datetime import datetime as Datetime
from typing import Final, Never from typing import Final, Never
import requests import requests
from pydantic import BaseModel, PositiveInt from pydantic import BaseModel, PositiveInt, SkipValidation
from requests import Response from requests import Response
from delta_barth.constants import HTTP_CURRENT_CONNECTION, KnownApiErrorCodes from delta_barth.constants import HTTP_CURRENT_CONNECTION, KnownDelBarApiErrorCodes
from delta_barth.errors import ( from delta_barth.errors import (
ApiConnectionError, ApiConnectionError,
UnknownApiErrorCode, UnknownApiErrorCode,
@ -113,7 +113,7 @@ def login(
if resp.status_code == 200: if resp.status_code == 200:
response = LoginResponse(**resp.json()) response = LoginResponse(**resp.json())
HTTP_CURRENT_CONNECTION.add_session_token(response.token) HTTP_CURRENT_CONNECTION.add_session_token(response.token)
elif resp.status_code in KnownApiErrorCodes.COMMON.value: elif resp.status_code in KnownDelBarApiErrorCodes.COMMON.value:
err = DelBarApiError(status_code=resp.status_code, **resp.json()) err = DelBarApiError(status_code=resp.status_code, **resp.json())
response = LoginResponse(token="", error=err) response = LoginResponse(token="", error=err)
else: # pragma: no cover else: # pragma: no cover
@ -142,7 +142,7 @@ def logout(
if resp.status_code == 200: if resp.status_code == 200:
response = LogoutResponse() response = LogoutResponse()
HTTP_CURRENT_CONNECTION.remove_session_token() HTTP_CURRENT_CONNECTION.remove_session_token()
elif resp.status_code in KnownApiErrorCodes.COMMON.value: elif resp.status_code in KnownDelBarApiErrorCodes.COMMON.value:
err = DelBarApiError(status_code=resp.status_code, **resp.json()) err = DelBarApiError(status_code=resp.status_code, **resp.json())
response = LogoutResponse(error=err) response = LogoutResponse(error=err)
else: # pragma: no cover else: # pragma: no cover
@ -153,8 +153,8 @@ def logout(
# ** sales data # ** sales data
class SalesPrognosisRequestP(BaseModel): class SalesPrognosisRequestP(BaseModel):
FirmaId: int | None FirmaId: SkipValidation[int | None]
BuchungsDatum: Datetime | None BuchungsDatum: SkipValidation[Datetime | None]
class SalesPrognosisResponseEntry(BaseModel): class SalesPrognosisResponseEntry(BaseModel):
@ -192,7 +192,7 @@ def get_sales_prognosis_data(
response: SalesPrognosisResponse response: SalesPrognosisResponse
if resp.status_code == 200: if resp.status_code == 200:
response = SalesPrognosisResponse(**resp.json()) response = SalesPrognosisResponse(**resp.json())
elif resp.status_code in KnownApiErrorCodes.COMMON.value: # pragma: no cover elif resp.status_code in KnownDelBarApiErrorCodes.COMMON.value: # pragma: no cover
err = DelBarApiError(status_code=resp.status_code, **resp.json()) err = DelBarApiError(status_code=resp.status_code, **resp.json())
response = SalesPrognosisResponse(daten=tuple(), error=err) response = SalesPrognosisResponse(daten=tuple(), error=err)
else: # pragma: no cover else: # pragma: no cover

View File

@ -3,7 +3,9 @@ from typing import Final
from delta_barth.types import CurrentConnection, HttpContentHeaders from delta_barth.types import CurrentConnection, HttpContentHeaders
# ** API connection management # ** error handling
DEFAULT_INTERNAL_ERR_CODE: Final[int] = 100
HTTP_BASE_CONTENT_HEADERS: Final[HttpContentHeaders] = { HTTP_BASE_CONTENT_HEADERS: Final[HttpContentHeaders] = {
"Content-type": "application/json", "Content-type": "application/json",
"Accept": "application/json", "Accept": "application/json",
@ -14,7 +16,7 @@ HTTP_CURRENT_CONNECTION: Final[CurrentConnection] = CurrentConnection(
) )
class KnownApiErrorCodes(enum.Enum): class KnownDelBarApiErrorCodes(enum.Enum):
COMMON = frozenset((400, 401, 409, 500)) COMMON = frozenset((400, 401, 409, 500))

View File

@ -1,3 +1,14 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Final
from delta_barth.constants import DEFAULT_INTERNAL_ERR_CODE
from delta_barth.types import DataPipelineErrors, doptResponseError
if TYPE_CHECKING:
from delta_barth.types import ErrorDescription
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"""
@ -12,3 +23,45 @@ class ApiConnectionError(Exception):
class FeaturesMissingError(Exception): class FeaturesMissingError(Exception):
"""exception raised if needed features are missing""" """exception raised if needed features are missing"""
## ** internal error handling
DATA_PIPELINE_ERRORS_DESCR: Final[tuple[ErrorDescription, ...]] = (
("SUCCESS", 0, "Erfolg"),
("TOO_FEW_POINTS", 1, "Datensatz besitzt nicht genügend Datenpunkte"),
("BAD_QUALITY", 2, "Prognosequalität des Modells unzureichend"),
)
class ErrorManager:
def __init__(self) -> None:
self._data_pipelines: DataPipelineErrors | None = None
self._parse_data_pipeline_errors()
@property
def data_pipelines(self) -> DataPipelineErrors:
assert self._data_pipelines is not None, (
"tried to access not parsed data pipeline errors"
)
return self._data_pipelines
def _parse_data_pipeline_errors(self) -> None:
if self._data_pipelines is not None:
return
parsed_errors: dict[str, doptResponseError] = {}
for err in DATA_PIPELINE_ERRORS_DESCR:
parsed_errors[err[0]] = doptResponseError(status_code=err[1], description=err[2])
self._data_pipelines = DataPipelineErrors(**parsed_errors)
def internal_error(
self,
description: str,
message: str = "",
err_code: int = DEFAULT_INTERNAL_ERR_CODE,
) -> doptResponseError:
return doptResponseError(
status_code=err_code,
description=description,
message=message,
)

View File

@ -4,9 +4,25 @@ from dataclasses import dataclass, field
from typing import NotRequired, TypeAlias, TypedDict from typing import NotRequired, TypeAlias, TypedDict
import pandas as pd import pandas as pd
from pydantic import BaseModel, SkipValidation
# ** API # ** API
ErrorDescription: TypeAlias = tuple[str, int, str]
class doptResponseError(BaseModel):
status_code: SkipValidation[int]
description: SkipValidation[str]
message: SkipValidation[str] = ""
@dataclass(slots=True)
class DataPipelineErrors:
SUCCESS: doptResponseError
TOO_FEW_POINTS: doptResponseError
BAD_QUALITY: doptResponseError
class HttpRequestTypes(enum.StrEnum): class HttpRequestTypes(enum.StrEnum):
GET = enum.auto() GET = enum.auto()
POST = enum.auto() POST = enum.auto()
@ -73,10 +89,4 @@ class CustomerDataSalesForecast:
sales: list[float] = field(default_factory=list) sales: list[float] = field(default_factory=list)
class FcErrorCodes(enum.IntEnum): FcResult: TypeAlias = tuple[DataPipelineErrors, pd.DataFrame | None]
SUCCESS = 0
DATA_TOO_FEW_POINTS = 1
DATA_BAD_QUALITY = 2
FcResult: TypeAlias = tuple[FcErrorCodes, pd.DataFrame | None]

View File

@ -105,8 +105,7 @@ def test_login_logout(credentials, api_base_url):
assert resp.error.message == "Nutzer oder Passwort falsch." assert resp.error.message == "Nutzer oder Passwort falsch."
# @pytest.mark.api_con_required @pytest.mark.api_con_required
@pytest.mark.new
def test_get_sales_prognosis_data(credentials, api_base_url): def test_get_sales_prognosis_data(credentials, api_base_url):
resp = common.login( resp = common.login(
base_url=api_base_url, base_url=api_base_url,

43
tests/test_errors.py Normal file
View File

@ -0,0 +1,43 @@
from __future__ import annotations
from dataclasses import asdict
from typing import Any, cast
import pytest
from delta_barth import errors
from delta_barth.types import doptResponseError
def test_error_manager_parsing():
predef_errs = errors.DATA_PIPELINE_ERRORS_DESCR
err_mgr = errors.ErrorManager()
assert err_mgr.data_pipelines is not None
parsed_pipe_errs = err_mgr.data_pipelines
parsed_pipe_errs = asdict(parsed_pipe_errs)
for err in predef_errs:
dopt_err = cast(doptResponseError, parsed_pipe_errs[err[0]])
assert isinstance(dopt_err, doptResponseError)
assert dopt_err.status_code == err[1]
assert dopt_err.description == err[2]
assert dopt_err.message == ""
err_mgr._parse_data_pipeline_errors()
def test_error_manager_internal():
DESCRIPTION = "test case"
MESSAGE = "an error occurred"
ERR_CODE = 101
err_mgr = errors.ErrorManager()
new_err = err_mgr.internal_error(
description=DESCRIPTION,
message=MESSAGE,
err_code=ERR_CODE,
)
assert new_err.status_code == ERR_CODE
assert new_err.description == DESCRIPTION
assert new_err.message == MESSAGE