288 lines
9.1 KiB
Python

from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING, Final
import requests
import sqlalchemy as sql
from dopt_basics.io import combine_route
import delta_barth.logging
from delta_barth import databases as db
from delta_barth.api.common import (
LoginRequest,
LoginResponse,
validate_credentials,
)
from delta_barth.constants import API_CON_TIMEOUT, DB_ECHO
from delta_barth.errors import STATUS_HANDLER
from delta_barth.logging import logger_session as logger
from delta_barth.types import DelBarApiError, Status
if TYPE_CHECKING:
from delta_barth.types import ApiCredentials, HttpContentHeaders
def validate_path(
str_path: str,
) -> Path:
path = Path(str_path).resolve()
if not path.exists():
raise FileNotFoundError(f"Provided path >{path}< seems not to exist.")
elif not path.is_dir():
raise FileNotFoundError(f"Provided path >{path}< seems not to be a directory.")
return path
class Session:
def __init__(
self,
base_headers: HttpContentHeaders,
db_folder: str = "data",
logging_folder: str = "logs",
) -> None:
self._setup: bool = False
self._data_path: Path | None = None
self._db_path: Path | None = None
self._db_folder = db_folder
self._db_engine: sql.Engine | None = None
self._logging_dir: Path | None = None
self._logging_folder = logging_folder
self._creds: ApiCredentials | None = None
self._base_url: str | None = None
self._headers = base_headers
self._session_token: str | None = None
self._logged_in: bool = False
def setup(self) -> None:
# at this point: no logging configured
assert not self._setup, "tried to setup session twice"
self._setup_logging()
self._setup_db_management()
self._setup = True
logger.info("[SESSION] Setup procedure successful")
@property
def data_path(self) -> Path:
assert self._data_path is not None, "accessed data path not set"
return self._data_path
@property
def db_engine(self) -> sql.Engine:
assert self._db_engine is not None, "accessed database engine not set"
return self._db_engine
@property
def db_path(self) -> Path:
if self._db_path is not None and self._setup:
return self._db_path
db_root = (self.data_path / self._db_folder).resolve()
db_path = db_root / "dopt-data.db"
if not db_root.exists():
db_root.mkdir(parents=False)
self._db_path = db_path
return self._db_path
def _setup_db_management(self) -> None:
self._db_engine = db.get_engine(self.db_path, echo=DB_ECHO)
db.metadata.create_all(self._db_engine)
logger.info("[SESSION] Successfully setup DB management")
@property
def logging_dir(self) -> Path:
if self._logging_dir is not None and self._setup:
return self._logging_dir
logging_dir = self.data_path / self._logging_folder
if not logging_dir.exists():
logging_dir.mkdir(parents=False)
self._logging_dir = logging_dir
return self._logging_dir
def _setup_logging(self) -> None:
delta_barth.logging.setup_logging(self.logging_dir)
logger.info("[SESSION] Successfully setup logging")
def disable_logging(self) -> None:
delta_barth.logging.disable_logging()
@property
def creds(self) -> ApiCredentials:
assert self._creds is not None, "accessed credentials not set"
return self._creds
def set_data_path(
self,
path: str,
):
self._data_path = validate_path(path)
self._setup = False
def set_credentials(
self,
username: str,
password: str,
database: str,
mandant: str,
) -> None:
if self.logged_in:
self.logout()
self._creds = validate_credentials(
username=username,
password=password,
database=database,
mandant=mandant,
)
@property
def base_url(self) -> str:
assert self._base_url is not None, "accessed base URL not set"
return self._base_url
def set_base_url(
self,
base_url: str,
) -> None:
if self.logged_in:
self.logout()
self._base_url = base_url
@property
def headers(self) -> HttpContentHeaders:
return self._headers
@property
def session_token(self) -> str | None:
return self._session_token
@property
def logged_in(self) -> bool:
return self._logged_in
def _add_session_token(
self,
token: str,
) -> None:
assert self.session_token is None, "tried overwriting existing API session token"
self._session_token = token
self._headers.update(DelecoToken=token)
self._logged_in = True
def _remove_session_token(self) -> None:
assert self.session_token is not None, (
"tried to delete non-existing API session token"
)
if "DelecoToken" in self.headers:
del self._headers["DelecoToken"]
self._session_token = None
self._logged_in = False
def login(
self,
) -> tuple[LoginResponse, Status]:
ROUTE: Final[str] = "user/login"
URL: Final = combine_route(self.base_url, ROUTE)
login_req = LoginRequest(
userName=self.creds.username,
password=self.creds.password,
databaseName=self.creds.database,
mandantName=self.creds.mandant,
)
empty_response = LoginResponse(token="")
try:
resp = requests.put(
URL,
login_req.model_dump_json(),
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(**resp.json())
status = STATUS_HANDLER.pipe_states.SUCCESS
self._add_session_token(response.token)
else:
response = empty_response
err = DelBarApiError(status_code=resp.status_code, **resp.json())
status = STATUS_HANDLER.api_error(err)
return response, status
def logout(
self,
) -> tuple[None, Status]:
ROUTE: Final[str] = "user/logout"
URL: Final = combine_route(self.base_url, ROUTE)
try:
resp = requests.put(
URL,
headers=self.headers, # type: ignore
timeout=API_CON_TIMEOUT,
)
except requests.exceptions.Timeout: # pragma: no cover
return None, STATUS_HANDLER.pipe_states.CONNECTION_TIMEOUT
except requests.exceptions.RequestException: # pragma: no cover
return None, STATUS_HANDLER.pipe_states.CONNECTION_ERROR
status: Status
if resp.status_code == 200:
status = STATUS_HANDLER.SUCCESS
self._remove_session_token()
else:
err = DelBarApiError(status_code=resp.status_code, **resp.json())
status = STATUS_HANDLER.api_error(err)
return None, status
def assert_login(
self,
) -> tuple[LoginResponse, Status]:
# check if login token is still valid
# re-login if necessary
if self.session_token is None:
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()
response, status = self.login()
else:
response = empty_response
err = DelBarApiError(status_code=resp.status_code, **resp.json())
status = STATUS_HANDLER.api_error(err)
return response, status