202 lines
5.4 KiB
Python
202 lines
5.4 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
from datetime import datetime as Datetime
|
|
from typing import Final, Never
|
|
|
|
import requests
|
|
from pydantic import BaseModel, PositiveInt, SkipValidation
|
|
from requests import Response
|
|
|
|
from delta_barth.constants import HTTP_CURRENT_CONNECTION, KnownDelBarApiErrorCodes
|
|
from delta_barth.errors import (
|
|
ApiConnectionError,
|
|
UnknownApiErrorCode,
|
|
UnspecifiedRequestType,
|
|
)
|
|
from delta_barth.types import HttpRequestTypes
|
|
|
|
LOGIN_ERROR_CODES_KNOWN: Final[frozenset[int]] = frozenset((400, 401, 409, 500))
|
|
|
|
|
|
class DelBarApiError(BaseModel):
|
|
status_code: int
|
|
message: str = ""
|
|
code: str | None = None
|
|
hints: str | None = None
|
|
type: str | None = None
|
|
title: str | None = None
|
|
errors: dict | None = None
|
|
traceId: str | None = None
|
|
|
|
|
|
def _raise_for_unknown_error(
|
|
resp: Response,
|
|
) -> Never:
|
|
raise UnknownApiErrorCode(
|
|
f"Unknown response code for request. Status Code: {resp.status_code}, "
|
|
f"Content: {resp.text}"
|
|
)
|
|
|
|
|
|
def _assert_login_status() -> None:
|
|
if not HTTP_CURRENT_CONNECTION.logged_in:
|
|
raise ApiConnectionError("Curent session is not logged in")
|
|
|
|
|
|
def _strip_url_components(string: str) -> str:
|
|
return re.sub(r"^[ /]+|[ /]+$", "", string)
|
|
|
|
|
|
def combine_route(base_url: str, route: str) -> str:
|
|
base_url = _strip_url_components(base_url)
|
|
route = _strip_url_components(route)
|
|
return "/".join((base_url, route))
|
|
|
|
|
|
def ping(
|
|
base_url: str,
|
|
method: HttpRequestTypes,
|
|
) -> Response:
|
|
ROUTE: Final[str] = "ping"
|
|
URL: Final = combine_route(base_url, ROUTE)
|
|
|
|
resp: Response
|
|
if method == HttpRequestTypes.GET:
|
|
resp = requests.get(URL)
|
|
elif method == HttpRequestTypes.PUT:
|
|
resp = requests.put(URL)
|
|
elif method == HttpRequestTypes.DELETE:
|
|
resp = requests.delete(URL)
|
|
else:
|
|
raise UnspecifiedRequestType(f"Request type {method} not defined for endpoint")
|
|
|
|
return resp
|
|
|
|
|
|
# ** login
|
|
class LoginRequest(BaseModel):
|
|
userName: str
|
|
password: str
|
|
databaseName: str
|
|
mandantName: str
|
|
|
|
|
|
class LoginResponse(BaseModel):
|
|
token: str
|
|
error: DelBarApiError | None = None
|
|
|
|
|
|
def login(
|
|
base_url: str,
|
|
user_name: str,
|
|
password: str,
|
|
database: str,
|
|
mandant: str,
|
|
) -> LoginResponse:
|
|
ROUTE: Final[str] = "user/login"
|
|
URL: Final = combine_route(base_url, ROUTE)
|
|
|
|
login_req = LoginRequest(
|
|
userName=user_name,
|
|
password=password,
|
|
databaseName=database,
|
|
mandantName=mandant,
|
|
)
|
|
resp = requests.put(
|
|
URL,
|
|
login_req.model_dump_json(),
|
|
headers=HTTP_CURRENT_CONNECTION.headers, # type: ignore
|
|
)
|
|
|
|
response: LoginResponse
|
|
if resp.status_code == 200:
|
|
response = LoginResponse(**resp.json())
|
|
HTTP_CURRENT_CONNECTION.add_session_token(response.token)
|
|
elif resp.status_code in KnownDelBarApiErrorCodes.COMMON.value:
|
|
err = DelBarApiError(status_code=resp.status_code, **resp.json())
|
|
response = LoginResponse(token="", error=err)
|
|
else: # pragma: no cover
|
|
_raise_for_unknown_error(resp)
|
|
|
|
return response
|
|
|
|
|
|
# ** logout
|
|
class LogoutResponse(BaseModel):
|
|
error: DelBarApiError | None = None
|
|
|
|
|
|
def logout(
|
|
base_url: str,
|
|
) -> LogoutResponse:
|
|
ROUTE: Final[str] = "user/logout"
|
|
URL: Final = combine_route(base_url, ROUTE)
|
|
|
|
resp = requests.put(
|
|
URL,
|
|
headers=HTTP_CURRENT_CONNECTION.headers, # type: ignore
|
|
)
|
|
|
|
response: LogoutResponse
|
|
if resp.status_code == 200:
|
|
response = LogoutResponse()
|
|
HTTP_CURRENT_CONNECTION.remove_session_token()
|
|
elif resp.status_code in KnownDelBarApiErrorCodes.COMMON.value:
|
|
err = DelBarApiError(status_code=resp.status_code, **resp.json())
|
|
response = LogoutResponse(error=err)
|
|
else: # pragma: no cover
|
|
_raise_for_unknown_error(resp)
|
|
|
|
return response
|
|
|
|
|
|
# ** sales data
|
|
class SalesPrognosisRequestP(BaseModel):
|
|
FirmaId: SkipValidation[int | None]
|
|
BuchungsDatum: SkipValidation[Datetime | None]
|
|
|
|
|
|
class SalesPrognosisResponseEntry(BaseModel):
|
|
artikelId: PositiveInt
|
|
firmaId: PositiveInt
|
|
betrag: float # negative values are filtered out later
|
|
menge: float # reasons for negative values unknown
|
|
buchungsDatum: Datetime
|
|
|
|
|
|
class SalesPrognosisResponse(BaseModel):
|
|
daten: tuple[SalesPrognosisResponseEntry, ...]
|
|
error: DelBarApiError | None = None
|
|
|
|
|
|
def get_sales_prognosis_data(
|
|
base_url: str,
|
|
company_id: int | None = None,
|
|
start_date: Datetime | None = None,
|
|
) -> SalesPrognosisResponse:
|
|
_assert_login_status()
|
|
ROUTE: Final[str] = "verkauf/umsatzprognosedaten"
|
|
URL: Final = combine_route(base_url, ROUTE)
|
|
|
|
sales_prog_req = SalesPrognosisRequestP(
|
|
FirmaId=company_id,
|
|
BuchungsDatum=start_date,
|
|
)
|
|
resp = requests.get(
|
|
URL,
|
|
params=sales_prog_req.model_dump(mode="json", exclude_none=True),
|
|
headers=HTTP_CURRENT_CONNECTION.headers, # type: ignore[argumentType]
|
|
)
|
|
|
|
response: SalesPrognosisResponse
|
|
if resp.status_code == 200:
|
|
response = SalesPrognosisResponse(**resp.json())
|
|
elif resp.status_code in KnownDelBarApiErrorCodes.COMMON.value: # pragma: no cover
|
|
err = DelBarApiError(status_code=resp.status_code, **resp.json())
|
|
response = SalesPrognosisResponse(daten=tuple(), error=err)
|
|
else: # pragma: no cover
|
|
_raise_for_unknown_error(resp)
|
|
|
|
return response
|