directly integrate login assertion in requests #27

Merged
foefl merged 2 commits from login_assertion into main 2025-04-30 06:46:54 +00:00
5 changed files with 66 additions and 127 deletions

View File

@ -7,11 +7,13 @@ import requests
from dopt_basics.io import combine_route from dopt_basics.io import combine_route
from pydantic import BaseModel, PositiveInt, SkipValidation from pydantic import BaseModel, PositiveInt, SkipValidation
from delta_barth.constants import API_CON_TIMEOUT from delta_barth.constants import API_CON_TIMEOUT, MAX_LOGIN_RETRIES
from delta_barth.errors import STATUS_HANDLER from delta_barth.errors import STATUS_HANDLER
from delta_barth.types import DelBarApiError, ExportResponse, ResponseType, Status from delta_barth.types import DelBarApiError, ExportResponse, ResponseType, Status
if TYPE_CHECKING: if TYPE_CHECKING:
from requests import Response
from delta_barth.session import Session from delta_barth.session import Session
@ -57,10 +59,10 @@ def get_sales_prognosis_data(
start_date: Datetime | None = None, start_date: Datetime | None = None,
) -> tuple[SalesPrognosisResponse, Status]: ) -> tuple[SalesPrognosisResponse, Status]:
# TODO check elimination of assertion for login, #25 # TODO check elimination of assertion for login, #25
_, status = session.assert_login() # _, status = session.assert_login()
if status != STATUS_HANDLER.SUCCESS: # if status != STATUS_HANDLER.SUCCESS:
response = SalesPrognosisResponse(daten=tuple()) # response = SalesPrognosisResponse(daten=tuple())
return response, status # return response, status
ROUTE: Final[str] = "verkauf/umsatzprognosedaten" ROUTE: Final[str] = "verkauf/umsatzprognosedaten"
URL: Final = combine_route(session.base_url, ROUTE) URL: Final = combine_route(session.base_url, ROUTE)
@ -70,13 +72,19 @@ def get_sales_prognosis_data(
BuchungsDatum=start_date, BuchungsDatum=start_date,
) )
empty_response = SalesPrognosisResponse(daten=tuple()) empty_response = SalesPrognosisResponse(daten=tuple())
resp: Response | None = None
try: try:
for attempt in range(1, (MAX_LOGIN_RETRIES + 1)):
resp = requests.get( resp = requests.get(
URL, URL,
params=sales_prog_req.model_dump(mode="json", exclude_none=True), params=sales_prog_req.model_dump(mode="json", exclude_none=True),
headers=session.headers, # type: ignore[argumentType] headers=session.headers, # type: ignore[argumentType]
timeout=API_CON_TIMEOUT, timeout=API_CON_TIMEOUT,
) )
if resp.status_code == 401:
_, status = session.relogin()
if status != STATUS_HANDLER.SUCCESS and attempt == MAX_LOGIN_RETRIES:
return empty_response, status
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
return empty_response, STATUS_HANDLER.pipe_states.CONNECTION_TIMEOUT return empty_response, STATUS_HANDLER.pipe_states.CONNECTION_TIMEOUT
except requests.exceptions.RequestException: except requests.exceptions.RequestException:
@ -84,6 +92,7 @@ def get_sales_prognosis_data(
response: SalesPrognosisResponse response: SalesPrognosisResponse
status: Status status: Status
assert resp is not None, "tried to use not defined response"
if resp.status_code == 200: if resp.status_code == 200:
response = SalesPrognosisResponse(**resp.json()) response = SalesPrognosisResponse(**resp.json())
status = STATUS_HANDLER.SUCCESS status = STATUS_HANDLER.SUCCESS

View File

@ -51,6 +51,8 @@ class KnownDelBarApiErrorCodes(enum.Enum):
# ** API # ** API
API_CON_TIMEOUT: Final[float] = 10.0 # secs to response API_CON_TIMEOUT: Final[float] = 10.0 # secs to response
MAX_LOGIN_RETRIES: Final[int] = 1
# ** API response parsing # ** API response parsing
# ** column mapping [API-Response --> Target-Features] # ** column mapping [API-Response --> Target-Features]
COL_MAP_SALES_PROGNOSIS: Final[DualDict[str, str]] = DualDict( COL_MAP_SALES_PROGNOSIS: Final[DualDict[str, str]] = DualDict(

View File

@ -292,44 +292,11 @@ class Session:
return None, status return None, status
def assert_login( def relogin(
self, self,
) -> tuple[LoginResponse, Status]: ) -> tuple[LoginResponse, Status]:
# check if login token is still valid
# re-login if necessary
if self.session_token is None: if self.session_token is None:
return self.login() return self.login()
# use known endpoint which requires a valid token in its header
# evaluate the response to decide if:
# current token is still valid, token is not valid, other errors occurred
ROUTE: Final[str] = "verkauf/umsatzprognosedaten"
URL: Final = combine_route(self.base_url, ROUTE)
params: dict[str, int] = {"FirmaId": 999999}
empty_response = LoginResponse(token="")
try:
resp = requests.get(
URL,
params=params,
headers=self.headers, # type: ignore
timeout=API_CON_TIMEOUT,
)
except requests.exceptions.Timeout: # pragma: no cover
return empty_response, STATUS_HANDLER.pipe_states.CONNECTION_TIMEOUT
except requests.exceptions.RequestException: # pragma: no cover
return empty_response, STATUS_HANDLER.pipe_states.CONNECTION_ERROR
response: LoginResponse
status: Status
if resp.status_code == 200:
response = LoginResponse(token=self.session_token)
status = STATUS_HANDLER.SUCCESS
elif resp.status_code == 401:
self._remove_session_token() self._remove_session_token()
response, status = self.login() return self.login()
else:
response = empty_response
err = DelBarApiError(status_code=resp.status_code, **resp.json())
status = STATUS_HANDLER.api_error(err)
return response, status

View File

@ -4,7 +4,6 @@ import pytest
import requests import requests
from delta_barth.api import requests as requests_ from delta_barth.api import requests as requests_
from delta_barth.api.common import LoginResponse
@pytest.mark.api_con_required @pytest.mark.api_con_required
@ -54,12 +53,11 @@ def test_get_sales_prognosis_data_Success(session):
@pytest.mark.api_con_required @pytest.mark.api_con_required
def test_get_sales_prognosis_data_FailLogin(session, mock_get): def test_get_sales_prognosis_data_NoAuth(session, mock_get):
session.login() code = 401
code = 500
json = { json = {
"message": "ServerError", "message": "ServerError",
"code": "TestExternalServerError", "code": "TestFailAuth",
"hints": "TestCase", "hints": "TestCase",
} }
mock_get.return_value.status_code = code mock_get.return_value.status_code = code
@ -76,6 +74,36 @@ def test_get_sales_prognosis_data_FailLogin(session, mock_get):
assert status.api_server_error.hints == json["hints"] assert status.api_server_error.hints == json["hints"]
def test_get_sales_prognosis_data_FailLogin(session, mock_get, mock_put):
code = 401
json = {
"message": "ServerError",
"code": "TestFailAuth",
"hints": "TestCase",
}
mock_get.return_value.status_code = code
mock_get.return_value.json.return_value = json
code_put = 500
json_put = {
"message": "ServerError",
"code": "TestUnknownError",
"hints": "TestCase",
}
mock_put.return_value.status_code = code_put
mock_put.return_value.json.return_value = json_put
resp, status = requests_.get_sales_prognosis_data(session, None, None)
assert resp is not None
assert len(resp.daten) == 0
assert status.code == 400
assert status.api_server_error is not None
assert status.api_server_error.status_code == code_put
assert status.api_server_error.message == json_put["message"]
assert status.api_server_error.code == json_put["code"]
assert status.api_server_error.hints == json_put["hints"]
@pytest.mark.api_con_required @pytest.mark.api_con_required
def test_get_sales_prognosis_data_FailApiServer(session, mock_get): def test_get_sales_prognosis_data_FailApiServer(session, mock_get):
code = 405 code = 405
@ -101,11 +129,6 @@ def test_get_sales_prognosis_data_FailApiServer(session, mock_get):
def test_get_sales_prognosis_data_FailGetTimeout(session, mock_get): def test_get_sales_prognosis_data_FailGetTimeout(session, mock_get):
mock_get.side_effect = requests.exceptions.Timeout("Test timeout") mock_get.side_effect = requests.exceptions.Timeout("Test timeout")
def assert_login():
return LoginResponse(token=""), requests_.STATUS_HANDLER.SUCCESS
session.assert_login = assert_login
resp, status = requests_.get_sales_prognosis_data(session, None, None) resp, status = requests_.get_sales_prognosis_data(session, None, None)
assert resp is not None assert resp is not None
assert len(resp.daten) == 0 assert len(resp.daten) == 0
@ -115,11 +138,6 @@ def test_get_sales_prognosis_data_FailGetTimeout(session, mock_get):
def test_get_sales_prognosis_data_FailGetRequestException(session, mock_get): def test_get_sales_prognosis_data_FailGetRequestException(session, mock_get):
mock_get.side_effect = requests.exceptions.RequestException("Test not timeout") mock_get.side_effect = requests.exceptions.RequestException("Test not timeout")
def assert_login():
return LoginResponse(token=""), requests_.STATUS_HANDLER.SUCCESS
session.assert_login = assert_login
resp, status = requests_.get_sales_prognosis_data(session, None, None) resp, status = requests_.get_sales_prognosis_data(session, None, None)
assert resp is not None assert resp is not None
assert len(resp.daten) == 0 assert len(resp.daten) == 0

View File

@ -314,11 +314,11 @@ def test_login_logout_FailApiServer(session, mock_put):
@pytest.mark.api_con_required @pytest.mark.api_con_required
def test_assert_login_SuccessLoggedOut(session): def test_relogin_SuccessLoggedOut(session):
assert session.session_token is None assert session.session_token is None
assert session._creds is not None assert session._creds is not None
# test logged out state # test logged out state
resp, status = session.assert_login() resp, status = session.relogin()
assert resp is not None assert resp is not None
assert status.code == 0 assert status.code == 0
assert session.session_token is not None assert session.session_token is not None
@ -327,74 +327,17 @@ def test_assert_login_SuccessLoggedOut(session):
@pytest.mark.api_con_required @pytest.mark.api_con_required
def test_assert_login_SuccessStillLoggedIn(session): def test_relogin_SuccessStillLoggedIn(session):
assert session.session_token is None assert session.session_token is None
assert session._creds is not None assert session._creds is not None
resp, status = session.login() resp, status = session.login()
resp, status = session.assert_login() old_token = session.session_token
assert old_token is not None
resp, status = session.relogin()
assert resp is not None assert resp is not None
assert status.code == 0 assert status.code == 0
assert session.session_token is not None assert session.session_token is not None
assert session.session_token != old_token
resp, status = session.logout() resp, status = session.logout()
assert status.code == 0 assert status.code == 0
@pytest.mark.api_con_required
def test_assert_login_ReloginNoValidAuth(session, mock_get):
code = 401
json = {
"message": "AuthentificationError",
"code": "TestAssertLoginAfter",
"hints": "TestCase",
}
mock_get.return_value.status_code = code
mock_get.return_value.json.return_value = json
resp, status = session.login()
resp, status = session.assert_login()
assert resp is not None
assert status.code == 0
assert session.session_token is not None
resp, status = session.logout()
assert status.code == 0
@pytest.mark.api_con_required
def test_assert_login_ReloginWrongToken(session):
# triggers code 401
assert session.session_token is None
assert session._creds is not None
_, status = session.login()
assert status.code == 0
session._session_token = "WRONGTOKEN"
resp, status = session.assert_login()
assert resp is not None
assert status.code == 0
assert session.session_token is not None
resp, status = session.logout()
assert status.code == 0
@pytest.mark.api_con_required
def test_assert_login_FailApiServer(session, mock_get):
code = 500
json = {
"message": "ServerError",
"code": "TestExternalServerError",
"hints": "TestCase",
}
mock_get.return_value.status_code = code
mock_get.return_value.json.return_value = json
resp, status = session.login()
resp, status = session.assert_login()
assert resp is not None
assert not resp.token
assert status.code == 400
assert status.api_server_error is not None
assert status.api_server_error.status_code == code
assert status.api_server_error.message == json["message"]
assert status.api_server_error.code == json["code"]
assert status.api_server_error.hints == json["hints"]