diff --git a/.bumpversion.toml b/.bumpversion.toml new file mode 100644 index 0000000..c1c47e5 --- /dev/null +++ b/.bumpversion.toml @@ -0,0 +1,39 @@ +[tool.bumpversion] +current_version = "0.1.0" +parse = """(?x) + (?P0|[1-9]\\d*)\\. + (?P0|[1-9]\\d*)\\. + (?P0|[1-9]\\d*) + (?: + - # dash separator for pre-release section + (?P[a-zA-Z-]+) # pre-release label + (?P0|[1-9]\\d*) # pre-release version number + )? # pre-release section is optional +""" +serialize = [ + "{major}.{minor}.{patch}-{pre_l}{pre_n}", + "{major}.{minor}.{patch}", +] +search = "{current_version}" +replace = "{new_version}" +regex = false +ignore_missing_version = false +ignore_missing_files = false +tag = true +sign_tags = false +tag_name = "v{new_version}" +tag_message = "Bump version: {current_version} → {new_version}" +allow_dirty = false +commit = true +message = "Bump version: {current_version} → {new_version}" +commit_args = "" +setup_hooks = [] +pre_commit_hooks = [] +post_commit_hooks = [] +[tool.bumpversion.parts.pre_l] +values = ["dev", "rc", "final"] +optional_value = "final" +[[tool.bumpversion.files]] +filename = ".version" +[[tool.bumpversion.files]] +filename = "__init__.py" diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/.version b/.version new file mode 100644 index 0000000..e69de29 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/__main__.py b/__main__.py index ca54153..ecbeb42 100644 --- a/__main__.py +++ b/__main__.py @@ -1,3 +1,3 @@ -from src import UI +from src.ui.userInterface import launch_gui as UI if __name__ == "__main__": - UI() \ No newline at end of file + UI() #:des \ No newline at end of file diff --git a/config/__init__.py b/config/__init__.py index cca5d9b..3863ba1 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -1 +1,2 @@ + from .config import Config diff --git a/config/config.py b/config/config.py index 106eaef..aac2ca0 100644 --- a/config/config.py +++ b/config/config.py @@ -1,3 +1,4 @@ + from typing import Optional from dataclasses import dataclass from omegaconf import OmegaConf, DictConfig diff --git a/docs/search.md b/docs/search.md index 91e1907..191a1af 100644 --- a/docs/search.md +++ b/docs/search.md @@ -4,6 +4,7 @@ Auf dieser Seite gibt es zwei Hauptfunktionen: die Suche und die Statistik. Standardmäßig wird die Statistik geöffnet. + ## Statistikbereich ### Suche @@ -24,6 +25,8 @@ In diesem Bereich kann die Suche nach Semesterapparaten durchgeführt werden. Su Die Suche kann durch Klicken auf den Button **Suchen** gestartet werden. Die Ergebnisse werden in der Tabelle darunter angezeigt. ### Suchergebnisse +!!! Info + Der Ergebnisbereich kann über den Vertikalen Slider verschoben werden, um mehr Platz für Tabelle, oder den Graphen zu schaffen. Hierzu mit der Maus auf den Raum zwischen den beiden Bereichen klicken und ziehen. ![Statistiksuchergebnisse](images/ss_stat_result.png) In diesem Bereich werden die Suchergebnisse angezeigt. Für jeden gefundenen Treffer wird eine Zeile angelegt: diff --git a/icons/__init__.py b/icons/__init__.py index d881932..a2873fd 100644 --- a/icons/__init__.py +++ b/icons/__init__.py @@ -1,3 +1,4 @@ + from .config import Icons icons = Icons() diff --git a/icons/config.py b/icons/config.py new file mode 100644 index 0000000..beff803 --- /dev/null +++ b/icons/config.py @@ -0,0 +1,59 @@ +from omegaconf import OmegaConf, DictConfig +from typing import Optional +import os +import sys + +class Icons: + _config: Optional[DictConfig] = None + + def __init__(self): + self._config = OmegaConf.load("icons/icons.yaml") + self.config_path = "icons/config.yaml" + + def save(self): + OmegaConf.save(self._config, self.config_path) + + @property + def icons(self): + return self._config.icons + + def get_icon(self, name: str): + return self.icons[name] + + def set_icon(self, name: str, value: str): + self._config.icons[name] = value + + @property + def path(self): + return self._config.icon_path + + @property + def colors(self): + return self._config.colors + + @colors.setter + def colors(self, value): + self._config.colors = value + + @property + def dark(self): + return self._config.colors.dark + + @property + def light(self): + return self._config.colors.light + + @property + def warning(self): + return self._config.colors.warning + + @property + def success(self): + return self._config.colors.success + + def set_color(self, name: str, value: str): + self._config.colors[name] = value + + @icons.setter + def icons(self, value): + self._config.icons = value diff --git a/icons/icons.yaml b/icons/icons.yaml index 5303e45..e69de29 100644 --- a/icons/icons.yaml +++ b/icons/icons.yaml @@ -1,28 +0,0 @@ -icon_path: icons/ - -dark_color: '#75FB4Cmail' -light_color: '#EA3323' -icons: - locked: locked.svg - logo: logo.ico - show_password: visibility_off.svg - hide_password: visibility_on.svg - settings: settings.svg - today: calendar_today.svg - save: save.svg - edit_note: edit_note.svg - warning: warning.svg - error: error.svg - mail: mail.svg - semester: semester.svg - template_fail: test_fail.svg - offAction: shutdown.svg - info: info.svg - help: help.svg - close: close.svg - notification: notification.svg - valid_true: check_success.svg - valid_false: check_fail.svg - edit: edit.svg - important_warn : red_warn.svg - person: person_add.svg \ No newline at end of file diff --git a/mail.py b/mail.py index def3d24..e4614f3 100644 --- a/mail.py +++ b/mail.py @@ -1,3 +1,4 @@ + import sys from PyQt6 import QtWidgets diff --git a/mail_vorlagen/test.eml b/mail_vorlagen/test.eml new file mode 100644 index 0000000..238f4c3 --- /dev/null +++ b/mail_vorlagen/test.eml @@ -0,0 +1,11 @@ +Subject: test new template +MIME-Version: 1.0 +Content-Type: text/html; charset="UTF-8" +Content-Transfer-Encoding: 8bit + + + + + + +

dklfdslködfskfd

\ No newline at end of file diff --git a/main.py b/main.py index 36a81d3..328ce8b 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,4 @@ -from src.logic import userInterface +from src.ui.userInterface import launch_gui as UI if __name__ == "__main__": - userInterface.launch_gui() - - # display_graph() + UI() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9e077ab --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,33 @@ +[project] +name = "semesterapparatsmanager" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "beautifulsoup4>=4.12.3", + "bump-my-version>=0.29.0", + "chardet>=5.2.0", + "darkdetect>=0.8.0", + "docx2pdf>=0.1.8", + "loguru>=0.7.3", + "mkdocs>=1.6.1", + "mkdocs-material>=9.5.49", + "mkdocs-material-extensions>=1.3.1", + "natsort>=8.4.0", + "omegaconf>=2.3.0", + "pandas>=2.2.3", + "pyqt6>=6.8.0", + "pyqtgraph>=0.13.7", + "python-docx>=1.1.2", + "pyzotero>=1.6.4", + "ratelimit>=2.2.1", + "requests>=2.32.3", +] + +[dependency-groups] +dev = [ + "bump-my-version>=0.29.0", + "icecream>=2.1.4", + "nuitka>=2.5.9", +] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..602a33c --- /dev/null +++ b/pytest.ini @@ -0,0 +1,12 @@ +[pytest] +# command should be *including --cov to generate coverage report +addopts = --cov +testpaths = tests +python_files = test_*.py +; Configuring pytest +; More info: https://docs.pytest.org/en/6.2.x/customize.html + +;Logging +; DATE FORMAT EXAMPLE: %Y-%m-%d %H:%M:%S +; log_cli_format = %(asctime)s %(levelname)-8s %(name)-8s %(message)s +; log_cli_date_format = %H:%M:%S diff --git a/src/__init__.py b/src/__init__.py index eb61399..79f2e53 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,11 +1,25 @@ -__version__ = "1.0.0" -__author__ = "Alexander Kirchner" +import sys from config import Config +import os +from loguru import logger as log +import sys settings = Config("config/config.yaml") - from .utils.icon import Icon -from .logic.log import MyLogger -from .ui.userInterface import launch_gui as UI +__version__ = "0.1.0" +__author__ = "Alexander Kirchner" + + +if not os.path.exists("logs"): + os.mkdir("logs") + # open and close the file to create it +logger = log +logger.remove() +logger.add("logs/application_info.log", rotation="1 week", level="INFO", enqueue=True) +logger.add("logs/application_error.log", rotation="1 week", level="ERROR", enqueue=True) +logger.add("logs/application_debug.log", rotation="1 week", level="DEBUG", enqueue=True) +logger.add("logs/application.log", rotation="1 week", enqueue=True) +# logger.add(sys.stderr, format="{time} {level} {message}", level="INFO") +logger.add(sys.stdout) diff --git a/src/backend/__init__.py b/src/backend/__init__.py index b29c94a..a5351f7 100644 --- a/src/backend/__init__.py +++ b/src/backend/__init__.py @@ -1,5 +1,6 @@ + from .admin_console import AdminCommands from .create_file import recreateElsaFile, recreateFile from .database import Database from .delete_temp_contents import delete_temp_contents as tempdelete -from .semester import generateSemesterByDate, generateSemesterByOffset +from .semester import Semester diff --git a/src/backend/admin_console.py b/src/backend/admin_console.py index fee467b..054a1c2 100644 --- a/src/backend/admin_console.py +++ b/src/backend/admin_console.py @@ -1,3 +1,4 @@ + import hashlib import random diff --git a/src/backend/create_file.py b/src/backend/create_file.py index b77f389..ceeff20 100644 --- a/src/backend/create_file.py +++ b/src/backend/create_file.py @@ -1,7 +1,6 @@ import os from pathlib import Path -from icecream import ic from src.backend.database import Database diff --git a/src/backend/database.py b/src/backend/database.py index 452ff73..850fd2d 100644 --- a/src/backend/database.py +++ b/src/backend/database.py @@ -5,9 +5,9 @@ import tempfile from pathlib import Path from src import settings from typing import Any, Dict, List, Optional, Tuple, Union -# from icecream import ic from omegaconf import OmegaConf import datetime +from src import logger from src.backend.db import ( CREATE_ELSA_FILES_TABLE, CREATE_ELSA_MEDIA_TABLE, @@ -22,15 +22,14 @@ from src.backend.db import ( CREATE_TABLE_USER, ) from src.errors import AppPresentError, NoResultError -from src.logic import ApparatData, BookData, Prof, Apparat, ELSA, logger as log +from src.logic import ApparatData, BookData, Prof, Apparat, ELSA from src.logic.constants import SEMAP_MEDIA_ACCOUNTS from src.utils import create_blob, dump_pickle, load_pickle -from .semester import generateSemesterByDate -from icecream import ic -from string import ascii_lowercase as lower, digits +from .semester import Semester +from string import ascii_lowercase as lower, digits, punctuation -ascii_lowercase = lower + digits +ascii_lowercase = lower + digits + punctuation # get the line that called the function class Database: database = settings.database @@ -62,7 +61,7 @@ class Database: # print(path) os.makedirs(path) if self.get_db_contents() == []: - log.critical("Database does not exist, creating tables") + logger.critical("Database does not exist, creating tables") self.create_tables() self.insertSubjects() @@ -146,12 +145,12 @@ class Database: """ conn = self.connect() cursor = conn.cursor() - log.info(f"Inserting {params} into database with query {query}") + logger.debug(f"Inserting {params} into database with query {query}") cursor.execute(query, params) conn.commit() self.close_connection(conn) - @log.catch + @logger.catch def query_db( self, query: str, args: Tuple = (), one: bool = False ) -> Union[Tuple, List[Tuple]]: @@ -181,14 +180,14 @@ class Database: # if "INSERT" in query: # log_message = f"Querying database with query {query}" - log.info(log_message) + logger.debug(log_message) try: cursor.execute(query, args) rv = cursor.fetchall() conn.commit() self.close_connection(conn) except sql.OperationalError as e: - log.error(f"Error in query: {e}") + logger.error(f"Error in query: {e}") return None return (rv[0] if rv else None) if one else rv @@ -211,7 +210,7 @@ class Database: t_query = ( f"SELECT bookdata FROM media WHERE app_id={app_id} AND prof_id={prof_id}" ) - log.info(t_query) + logger.debug(t_query) # # print(t_query) result = cursor.execute(t_query).fetchall() result = [load_pickle(i[0]) for i in result] @@ -238,7 +237,7 @@ class Database: params = (converted, app_id, prof_id, 0) cursor.execute(query, params) logMessage = f"Added book with signature {bookdata.signature} to database, data: {converted}" - log.info(logMessage) + logger.info(logMessage) conn.commit() self.close_connection(conn) @@ -307,7 +306,7 @@ class Database: list[tuple[BookData, int]]: A list of tuples containing the wrapped Metadata and the id of the book """ rdata = self.query_db("SELECT * FROM media WHERE deleted=0") - # ic(rdata, len(rdata)) + # logger.debug(rdata, len(rdata)) mode = 0 if len(data) == 1: if "signature" in data.keys(): @@ -335,7 +334,7 @@ class Database: and data["title"] in bookdata.title ): ret.append((bookdata, app_id, prof_id)) - # ic(ret) + # logger.debug(ret) return ret def setAvailability(self, book_id: str, available: str): @@ -514,7 +513,7 @@ class Database: (app_id, prof_id), ) - def getSemersters(self) -> list[str]: + def getSemesters(self) -> list[str]: """Return all the unique semesters in the database Returns: @@ -640,7 +639,7 @@ class Database: Args: message_id (str): the id of the message """ - log.info(f"Deleting message with id {message_id}") + logger.debug(f"Deleting message with id {message_id}") self.query_db("DELETE FROM messages WHERE id=?", (message_id,)) # Prof data @@ -710,7 +709,6 @@ class Database: (profname.replace(",", ""),), one=True, ) - print(data) person = Prof() return person.from_tuple(data) @@ -770,20 +768,21 @@ class Database: if result is None: raise NoResultError("No result found") apparat = ApparatData() - apparat.appname = result[1] - apparat.appnr = result[4] - apparat.dauerapp = True if result[7] == 1 else False - prof_data = self.getProfData(self.getProfNameById(result[2])) - apparat.profname = self.getProfNameById(result[2]) - apparat.prof_mail = prof_data.mail - apparat.prof_tel = prof_data.telnr - apparat.prof_title = prof_data.title - apparat.app_fach = result[3] - apparat.erstellsemester = result[5] - apparat.semester = result[8] - apparat.deleted = result[9] - apparat.apparat_adis_id = result[11] - apparat.prof_adis_id = result[12] + apparat.apparat.id = result[0] + apparat.apparat.name = result[1] + apparat.apparat.appnr = result[4] + apparat.apparat.eternal = True if result[7] == 1 else False + apparat.prof = self.getProfData(self.getProfNameById(result[2])) + apparat.prof.fullname = self.getProfNameById(result[2]) + apparat.apparat.prof_id = result[2] + + apparat.apparat.subject = result[3] + apparat.apparat.created_semester = result[5] + apparat.apparat.extend_until = result[8] + apparat.apparat.deleted = result[9] + apparat.apparat.apparat_id_adis = result[11] + apparat.apparat.prof_id_adis = result[12] + apparat.apparat.konto = result[13] return apparat def getUnavailableApparatNumbers(self) -> List[int]: @@ -796,7 +795,7 @@ class Database: "SELECT appnr FROM semesterapparat WHERE deletion_status=0" ) numbers = [i[0] for i in numbers] - log.info(f"Currently used apparat numbers: {numbers}") + logger.info(f"Currently used apparat numbers: {numbers}") return numbers def setNewSemesterDate(self, app_id: Union[str, int], newDate, dauerapp=False): @@ -850,21 +849,22 @@ class Database: Returns: Optional[int]: the id of the apparat """ - ic(apparat) - prof = self.getProfByName(apparat.prof_details.fullname) - prof_id = prof.id - ic(prof_id, apparat.profname) - - app_id = self.getApparatId(apparat.appname) + logger.debug(apparat) + app = apparat.apparat + prof = apparat.prof + present_prof = self.getProfByName(prof.name()) + prof_id = present_prof.id + logger.debug(present_prof) + + app_id = self.getApparatId(app.name) if app_id: return AppPresentError(app_id) if not prof_id: - print("prof id not present, creating prof with data", apparat.prof_details) - prof_id = self.createProf(apparat.prof_details) - # self.getProfId(apparat.profname) - ic(prof_id) - query = f"INSERT OR IGNORE INTO semesterapparat (appnr, name, erstellsemester, dauer, prof_id, fach,deletion_status,konto) VALUES ('{apparat.appnr}', '{apparat.appname}', '{apparat.semester}', '{apparat.dauerapp}', {prof_id}, '{apparat.app_fach}', '{0}', '{SEMAP_MEDIA_ACCOUNTS[apparat.appnr]}')" - log.info(query) + logger.debug("prof id not present, creating prof with data", prof) + prof_id = self.createProf(prof) + logger.debug(prof_id) + query = f"INSERT OR IGNORE INTO semesterapparat (appnr, name, erstellsemester, dauer, prof_id, fach,deletion_status,konto) VALUES ('{app.appnr}', '{app.name}', '{app.created_semester}', '{app.eternal}', {prof_id}, '{app.subject}', '{0}', '{SEMAP_MEDIA_ACCOUNTS[app.appnr]}')" + logger.debug(query) self.query_db(query) return None def getApparatsByProf(self, prof_id: Union[str, int]) -> list[tuple]: @@ -934,7 +934,7 @@ class Database: """ conn = self.connect() cursor = conn.cursor() - semesters = self.getSemersters() + semesters = self.getSemesters() created = [] deleted = [] for semester in semesters: @@ -946,7 +946,6 @@ class Database: deleted.append(result[0]) # store data in a tuple ret = [] - e_tuple = () for sem in semesters: e_tuple = ( sem, @@ -957,17 +956,16 @@ class Database: self.close_connection(conn) return ret - def deleteApparat(self, app_id: Union[str, int]): + def deleteApparat(self, app_id: Union[str, int], semester): """Delete an apparat from the database Args: app_id (Union[str, int]): the id of the apparat semester (str): the semester the apparat should be deleted from """ - today = datetime.datetime.now().strftime("%Y-%m-%d") self.query_db( "UPDATE semesterapparat SET deletion_status=1, deleted_date=? WHERE appnr=?", - (today, app_id), + (semester, app_id), ) def isEternal(self, id): @@ -1007,15 +1005,15 @@ class Database: """ query = "UPDATE semesterapparat SET name = ?, fach = ?, dauer = ?, prof_id = ?, prof_id_adis = ?, apparat_id_adis = ? WHERE appnr = ?" params = ( - apparat_data.appname, - apparat_data.app_fach, - apparat_data.dauerapp, - self.getProfData(apparat_data.prof_details.fullname).id, - apparat_data.prof_adis_id, - apparat_data.apparat_adis_id, - apparat_data.appnr, + apparat_data.apparat.name, + apparat_data.apparat.subject, + apparat_data.apparat.eternal, + self.getProfData(apparat_data.prof.fullname).id, + apparat_data.apparat.prof_id_adis, + apparat_data.apparat.apparat_id_adis, + apparat_data.apparat.appnr, ) - log.info(f"Updating apparat with query {query} and params {params}") + logger.debug(f"Updating apparat with query {query} and params {params}") self.query_db(query, params) def checkApparatExists(self, app_name: str): @@ -1067,7 +1065,8 @@ class Database: Returns: list: the result of the query """ - ic(query) + logger.debug(query) + logger.debug(f"Query: {query}") conn = self.connect() cursor = conn.cursor() result = cursor.execute(query).fetchall() @@ -1080,15 +1079,15 @@ class Database: result_a = tuple(result_a) result[result.index(orig_value)] = result_a self.close_connection(conn) - log.info(f"Query result: {result}") + logger.debug(f"Query result: {result}") return result if "deletable" in kwargs.keys(): query = f"""SELECT * FROM semesterapparat WHERE deletion_status=0 AND dauer=0 AND ( - (erstellsemester!='{kwargs['deletesemester']}' AND verlängerung_bis IS NULL) OR - (erstellsemester!='{kwargs['deletesemester']}' AND verlängerung_bis!='{kwargs['deletesemester']}' AND verlängerung_bis!='{generateSemesterByDate(True)}') + (erstellsemester!='{kwargs["deletesemester"]}' AND verlängerung_bis IS NULL) OR + (erstellsemester!='{kwargs["deletesemester"]}' AND verlängerung_bis!='{kwargs["deletesemester"]}' AND verlängerung_bis!='{Semester()}') )""" return __query(query) if "dauer" in kwargs.keys(): @@ -1119,8 +1118,10 @@ class Database: f"(erstellsemester='{kwargs['endsemester']}' OR verlängerung_bis='{kwargs['endsemester']}') AND ", ) # remove all x="" parts from the query where x is a key in kwargs - query = query[:-5] + logger.info(f"Query before: {query}") query = query.strip() + query = query[:-4] + logger.info(f"Query after: {query}") # check if query ends with lowercase letter or a '. if not, remove last symbol and try again while query[-1] not in ascii_lowercase and query[-1] != "'": query = query[:-1] @@ -1471,22 +1472,19 @@ class Database: ) ### - - - def createProf(self, profdata:Prof): - print("createProf") - ic(profdata) + def createProf(self, profdata: Prof): + logger.debug(profdata) conn = self.connect() cursor = conn.cursor() - fname = profdata.firstname#profdata["profname"].split(", ")[1].strip() - lname = profdata.lastname#profdata["profname"].split(", ")[0].strip() + fname = profdata.firstname + lname = profdata.lastname fullname = f"{lname} {fname}" - mail = profdata.mail#profdata["prof_mail"] - telnr = profdata.telnr#profdata["prof_tel"] - title = profdata.title #profdata["title"] + mail = profdata.mail + telnr = profdata.telnr + title = profdata.title query = f"INSERT INTO prof (fname, lname, fullname, mail, telnr,titel) VALUES ('{fname}','{lname}','{fullname}','{mail}','{telnr}','{title}')" - log.info(query) + logger.debug(query) cursor.execute(query) conn.commit() @@ -1519,7 +1517,7 @@ class Database: conn = self.connect() cursor = conn.cursor() if isinstance(profdata, Prof): - fullname = profdata.fullname + fullname = profdata.name() else: name = profdata["profname"] if ","in name: @@ -1529,7 +1527,7 @@ class Database: else: fullname = profdata["profname"] query = f"SELECT id FROM prof WHERE fullname = '{fullname}'" - log.info(query) + logger.debug(query) cursor.execute(query) result = cursor.fetchone() @@ -1546,7 +1544,7 @@ class Database: conn = self.connect() cursor = conn.cursor() query = f"SELECT * FROM prof WHERE fullname = '{fullname}'" - log.info(query) + logger.debug(query) result = cursor.execute(query).fetchone() if result: diff --git a/src/backend/db.py b/src/backend/db.py index 16cb35c..24f5565 100644 --- a/src/backend/db.py +++ b/src/backend/db.py @@ -1,3 +1,4 @@ + CREATE_TABLE_APPARAT = """CREATE TABLE semesterapparat ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT, diff --git a/src/backend/delete_temp_contents.py b/src/backend/delete_temp_contents.py index c2c147d..f60569d 100644 --- a/src/backend/delete_temp_contents.py +++ b/src/backend/delete_temp_contents.py @@ -1,8 +1,9 @@ + import os from pathlib import Path -from src import database - +from src import settings +database = settings.database def delete_temp_contents(): """ diff --git a/src/backend/pickles.py b/src/backend/pickles.py index 22f60c9..6a78b2f 100644 --- a/src/backend/pickles.py +++ b/src/backend/pickles.py @@ -1,3 +1,4 @@ + import pickle from typing import Any, ByteString diff --git a/src/backend/semester.py b/src/backend/semester.py index ce5a18b..17e9ca6 100644 --- a/src/backend/semester.py +++ b/src/backend/semester.py @@ -1,24 +1,21 @@ import datetime -from src.logic.log import logger +from src import logger from dataclasses import dataclass @dataclass class Semester: logger.debug("Semester class loaded") - _year: int | None = datetime.datetime.now().year + _year: int | None = str(datetime.datetime.now().year)[2:] _semester: str | None = None _month: int | None = datetime.datetime.now().month value: str = None - logger.debug(f"Got values year:{_year}, semester:{_semester}, month:{_month}") def __post_init__(self): - logger.debug( - f"Got values post_init year:{self._year}, semester:{self._semester}, month:{self._month}" - ) - + if isinstance(self._year, str): + self._year = int(self._year) if self._year is None: - self._year = datetime.datetime.now().year + self._year = datetime.datetime.now().year[2:] if self._month is None: self._month = datetime.datetime.now().month if self._semester is None: @@ -37,11 +34,7 @@ class Semester: @logger.catch def computeValue(self): # year is only last two digits - year = str(self._year)[2:] - year = int(year) - logger.debug( - f"Year is {year}, month is {self._month}, semester is {self._semester}" - ) + year = self._year if self._semester == "WiSe": if self._month < 4: valueyear = str(year - 1) + "/" + str(year) @@ -60,29 +53,75 @@ class Semester: str: the new semester value """ assert isinstance(value, int), "Value must be an integer" - logger.debug(f"Offsetting semester by {value}") if value == 0: return self if value > 0: if value % 2 == 0: - return Semester(self._year + value // 2, self._semester) + return Semester( + self._year - value // 2, self._semester - value // 2 + 1 + ) else: semester = self._semester semester = "SoSe" if semester == "WiSe" else "WiSe" - logger.debug(f"Semester is {semester}") return Semester(self._year + value // 2, semester) else: if value % 2 == 0: - return Semester(self._year - value // 2, self._semester) + return Semester(self.year + value // 2, self._semester) else: semester = self._semester semester = "SoSe" if semester == "WiSe" else "WiSe" return Semester(self._year + value // 2, semester) + def isPastSemester(self, semester) -> bool: + """Checks if the current Semester is a past Semester compared to the given Semester + + Args: + semester (str): The semester to compare to + + Returns: + bool: True if the current semester is in the past, False otherwise + """ + if self.year < semester.year: + return True + if self.year == semester.year: + if self.semester == "WiSe" and semester.semester == "SoSe": + return True + return False + + def isFutureSemester(self, semester: "Semester") -> bool: + """Checks if the current Semester is a future Semester compared to the given Semester + + Args: + semester (str): The semester to compare to + + Returns: + bool: True if the current semester is in the future, False otherwise + """ + if self.year > semester.year: + return True + if self.year == semester.year: + if self.semester == "SoSe" and semester.semester == "WiSe": + return True + return False + + def from_string(self, val): + self.value = val + self._year = int(val[-2:]) + self._semester = val[:4] + return self + @property def next(self): return self.offset(1) @property def previous(self): - return self.offset(-1) \ No newline at end of file + return self.offset(-1) + + @property + def year(self): + return self._year + + @property + def semester(self): + return self._semester diff --git a/src/backend/settings.py b/src/backend/settings.py index 1390d78..fc6d2ce 100644 --- a/src/backend/settings.py +++ b/src/backend/settings.py @@ -1,3 +1,4 @@ + from dataclasses import dataclass, field import yaml diff --git a/src/errors/DatabaseErrors.py b/src/errors/DatabaseErrors.py index 0b55a41..af268e6 100644 --- a/src/errors/DatabaseErrors.py +++ b/src/errors/DatabaseErrors.py @@ -1,3 +1,4 @@ + class NoResultError(Exception): def __init__(self, message): self.message = f"The query: {message} returned no results" diff --git a/src/errors/__init__.py b/src/errors/__init__.py index 0a24916..298340b 100644 --- a/src/errors/__init__.py +++ b/src/errors/__init__.py @@ -1,2 +1,3 @@ + # import basic error classes from .DatabaseErrors import * diff --git a/src/logic/__init__.py b/src/logic/__init__.py index eeab8b3..7d66061 100644 --- a/src/logic/__init__.py +++ b/src/logic/__init__.py @@ -1,9 +1,9 @@ -from .log import MyLogger, logger + from .dataclass import ApparatData, BookData, Prof, Apparat, ELSA from .thread_bookgrabber import BookGrabber from .threads_autoadder import AutoAdder from .threads_availchecker import AvailChecker -from .c_sort import custom_sort +from .c_sort import custom_sort, sort_semesters_list from .constants import APP_NRS, PROF_TITLES, SEMAP_MEDIA_ACCOUNTS from .csvparser import csv_to_list from .wordparser import elsa_word_to_csv, word_docx_to_csv diff --git a/src/logic/c_sort.py b/src/logic/c_sort.py index eeb9dae..ef0e4c7 100644 --- a/src/logic/c_sort.py +++ b/src/logic/c_sort.py @@ -1,59 +1,86 @@ -from typing import List, Tuple - -from natsort import natsorted - - -def custom_sort(unsorted: List[Tuple[str, int, int]]) -> List[Tuple[str, int, int]]: - """Sort a list of semesters in the format "SoSe n" and "WiSe n/n+1" in the correct order. - Where n == year in 2 digit format - - Args: - ---- - unsorted (list[tuple]): List of semesters in the format "SoSe n" and "WiSe n/n+1" - - Returns: - ------- - ret (list[tuple]): Sorted list in correct order of WiSe n/n+1 and SoSe n +def parse_semester(semester: str): """ - summer = natsorted([i for i in unsorted if "SoSe" in i[0]]) - winter = natsorted([i for i in unsorted if "WiSe" in i[0]]) - summer = natsorted(summer, key=lambda x: x[0]) - winter = natsorted(winter, key=lambda x: x[0]) + Parses the semester string into a sortable format. + Returns a tuple of (year, type), where type is 0 for SoSe and 1 for WiSe. + """ + if semester.startswith("SoSe"): + return int(semester.split()[1]), 0 + elif semester.startswith("WiSe"): + year_part = semester.split()[1] + start_year, _ = map(int, year_part.split("/")) + return start_year, 1 + else: + raise ValueError(f"Invalid semester format: {semester}") - # Merge the lists - ret = [] - i = 0 - j = 0 - while i < len(summer) and j < len(winter): - if summer[i][0][5:] <= winter[j][0][5:]: - ret.append(summer[i]) - i += 1 + +def custom_sort(entries): + """ + Sorts the list of tuples based on the custom schema. + + :param entries: List of tuples in the format (str, int, int). + :return: Sorted list of tuples. + """ + return sorted( + entries, + key=lambda entry: ( + parse_semester(entry[0]), # Sort by semester parsed as (year, type) + entry[1], # Then by the second element of the tuple + entry[2], # Finally by the third element of the tuple + ), + ) + + +def parse_semester(semester: str): + """ + Parses the semester string into a sortable format. + Returns a tuple of (year, type), where type is 0 for SoSe and 1 for WiSe. + """ + if semester.startswith("SoSe"): + return int(semester.split()[1]), 0 + elif semester.startswith("WiSe"): + year_part = semester.split()[1] + if "/" in year_part: + start_year, _ = map(int, year_part.split("/")) else: - ret.append(winter[j]) - j += 1 + start_year = int(year_part) + return start_year, 1 + else: + raise ValueError(f"Invalid semester format: {semester}") - # Append the remaining items - while i < len(summer): - ret.append(summer[i]) - i += 1 - while j < len(winter): - ret.append(winter[j]) - j += 1 - return ret +def sort_semesters_list(semesters: list) -> list: + """ + Sorts a list of semester strings based on year and type. - # Test the function - - pass + :param semesters: List of semester strings (e.g., "SoSe 24", "WiSe 22/23"). + :return: Sorted list of semester strings. + """ + return sorted(semesters, key=parse_semester) if __name__ == "__main__": unsorted = [ - ("WiSe 23/24", 7, 5), - ("SoSe 23", 5, 0), - ("SoSe 22", 1, 0), - ("WiSe 22/23", 1, 0), - ("SoSe 15", 1, 0), + "SoSe 24", + "WiSe 22/23", + "WiSe 23/24", + "WiSe 20/21", + "SoSe 23", + "SoSe 20", + "WiSe 7/8", + "WiSe 14/15", + "WiSe 13/14", + "SoSe 8", + "WiSe 19/20", + "WiSe 12/13", + "WiSe 21/22", + "WiSe 18/19", + "WiSe 11/12", + "WiSe 9/10", + "WiSe 6/7", + "SoSe 7", + "WiSe 16/17", + "WiSe 24/25", + "SoSe 25", ] - # print(custom_sort(unsorted)) + print(sort_semesters_list(unsorted)) diff --git a/src/logic/constants.py b/src/logic/constants.py index f2b4223..579a7e7 100644 --- a/src/logic/constants.py +++ b/src/logic/constants.py @@ -1,3 +1,4 @@ + from enum import Enum APP_NRS = [i for i in range(1, 181)] diff --git a/src/logic/csvparser.py b/src/logic/csvparser.py index c9be28f..60c9a9d 100644 --- a/src/logic/csvparser.py +++ b/src/logic/csvparser.py @@ -1,3 +1,4 @@ + import csv import chardet diff --git a/src/logic/dataclass.py b/src/logic/dataclass.py index b8725cb..e1bd127 100644 --- a/src/logic/dataclass.py +++ b/src/logic/dataclass.py @@ -1,18 +1,20 @@ import re from dataclasses import dataclass, field + from enum import Enum + @dataclass class Prof: id: int = None _title: str = None - firstname: str= None - lastname: str= None - fullname: str= None - mail: str= None - telnr: str= None - - #add function that sets the data based on a dict + firstname: str = None + lastname: str = None + fullname: str = None + mail: str = None + telnr: str = None + + # add function that sets the data based on a dict def from_dict(self, data: dict): for key, value in data.items(): if hasattr(self, key): @@ -28,7 +30,8 @@ class Prof: @title.setter def title(self, value): self._title = value - #add function that sets the data from a tuple + + # add function that sets the data from a tuple def from_tuple(self, data: tuple): setattr(self, "id", data[0]) setattr(self, "_title", data[1]) @@ -40,40 +43,18 @@ class Prof: return self def name(self, comma=False): + if self.firstname is None and self.lastname is None: + if "," in self.fullname: + self.firstname = self.fullname.split(",")[1].strip() + self.lastname = self.fullname.split(",")[0].strip() + else: + return self.fullname + if comma: return f"{self.lastname}, {self.firstname}" return f"{self.lastname} {self.firstname}" -@dataclass -class ApparatData: - prof_title: str | None = None - profname: str | None = None - dauerapp: bool = False - appnr: int | None = None - appname: str | None = None - app_fach: str | None = None - semester: str | None = None - erstellsemester: str | None = None - prof_mail: str | None = None - prof_tel: int | None = None - deleted: int = 0 - prof_adis_id: int | None = None - apparat_adis_id: int | None = None - @property - def prof_details(self) -> Prof: - return Prof().from_dict({ - "title": self.prof_title, - "firstname": self.profname.split(',')[1].strip(), - "lastname": self.profname.split(',')[0].strip(), - "mail": self.prof_mail, - "telnr": self.prof_tel, - "fullname": f"{self.profname.split(',')[0].strip()} {self.profname.split(',')[1].strip()}", - }) - def translateToFullname(self): - - return f"{self.profname.split(',')[0].strip()} {self.profname.split(',')[1].strip()}" - @dataclass class BookData: ppn: str | None = None @@ -132,7 +113,7 @@ class Subjects(Enum): FRENCH = (6, "Französisch") GEOGRAPHY = (7, "Geographie") HISTORY = (8, "Geschichte") - HEALT_EDUCATION = (9, "Gesundheitspädagogik") + HEALTH_EDUCATION = (9, "Gesundheitspädagogik") HTW = (10, "Haushalt / Textil") ART = (11, "Kunst") MATH_IT = (12, "Mathematik / Informatik") @@ -197,6 +178,13 @@ class Apparat: setattr(self, "konto", data[13]) return self + @property + def get_semester(self): + if self.extend_until is not None: + return self.extend_until + else: + return self.created_semester + @dataclass class ELSA: @@ -211,3 +199,7 @@ class ELSA: setattr(self, "semester", data[2]) setattr(self, "prof_id", data[3]) return self +@dataclass +class ApparatData: + prof: Prof = field(default_factory=Prof) + apparat: Apparat = field(default_factory=Apparat) diff --git a/src/logic/fileparser.py b/src/logic/fileparser.py index d527daa..bebe66e 100644 --- a/src/logic/fileparser.py +++ b/src/logic/fileparser.py @@ -1,3 +1,4 @@ + import csv import pandas as pd diff --git a/src/logic/mail.py b/src/logic/mail.py index e69de29..8b13789 100644 --- a/src/logic/mail.py +++ b/src/logic/mail.py @@ -0,0 +1 @@ + diff --git a/src/logic/settings.py b/src/logic/settings.py index 3b4754b..fe501a9 100644 --- a/src/logic/settings.py +++ b/src/logic/settings.py @@ -1,3 +1,4 @@ + from dataclasses import dataclass, field import yaml diff --git a/src/logic/thread_bookgrabber.py b/src/logic/thread_bookgrabber.py index 93675e5..1bf047c 100644 --- a/src/logic/thread_bookgrabber.py +++ b/src/logic/thread_bookgrabber.py @@ -3,7 +3,7 @@ import sqlite3 from PyQt6.QtCore import QThread from PyQt6.QtCore import pyqtSignal as Signal from src.backend.database import Database -from src.logic.log import MyLogger + from src.logic.webrequest import BibTextTransformer, WebRequest @@ -16,8 +16,7 @@ class BookGrabber(QThread): def __init__(self, appnr): super(BookGrabber, self).__init__(parent=None) self.is_Running = True - self.logger = MyLogger("Worker") - self.logger.log_info("Starting worker thread") + logger.info("Starting worker thread") self.data = None self.app_id = None self.prof_id = None @@ -35,9 +34,9 @@ class BookGrabber(QThread): self.data = data self.use_any = any_book self.use_exact = exact - self.logger.log_info(f"Working on {len(self.data)} entries") + logger.info(f"Working on {len(self.data)} entries") self.tstate = (self.app_id, self.prof_id, self.mode, self.data) - self.logger.log_debug("State: " + str(self.tstate)) + logger.debug("State: " + str(self.tstate)) # print(self.tstate) def run(self): @@ -50,7 +49,7 @@ class BookGrabber(QThread): for entry in iterdata: # print(entry) signature = str(entry) - self.logger.log_info("Processing entry: " + signature) + logger.info("Processing entry: " + signature) webdata = WebRequest().set_apparat(self.appnr).get_ppn(entry) if self.use_any: @@ -79,7 +78,7 @@ class BookGrabber(QThread): self.db.addBookToDatabase(bd, self.app_id, self.prof_id) # get latest book id self.book_id = self.db.getLastBookId() - self.logger.log_info("Added book to database") + logger.info("Added book to database") state = 0 for result in transformer.RDS_DATA: # print(result.RDS_LOCATION) @@ -87,17 +86,17 @@ class BookGrabber(QThread): state = 1 break - self.logger.log_info(f"State of {signature}: {state}") + logger.info(f"State of {signature}: {state}") # print("updating availability of " + str(self.book_id) + " to " + str(state)) try: self.db.setAvailability(self.book_id, state) except sqlite3.OperationalError as e: - self.logger.log_error(f"Failed to update availability: {e}") + logger.error(f"Failed to update availability: {e}") # time.sleep(5) item += 1 self.updateSignal.emit(item, len(self.data)) - self.logger.log_info("Worker thread finished") + logger.info("Worker thread finished") # self.done.emit() self.quit() @@ -111,17 +110,17 @@ class BookGrabber(QThread): # def __init__(self, app_id, prof_id, mode, data, parent=None): # super(BookGrabber, self).__init__(parent=None) # self.is_Running = True -# self.logger = MyLogger("Worker") -# self.logger.log_info("Starting worker thread") +# logger = MyLogger("Worker") +# logger.info("Starting worker thread") # self.data = data -# self.logger.log_info(f"Working on {len(self.data)} entries") +# logger.info(f"Working on {len(self.data)} entries") # self.app_id = app_id # self.prof_id = prof_id # self.mode = mode # self.book_id = None # self.state = (self.app_id, self.prof_id, self.mode, self.data) # # print(self.state) -# self.logger.log_info("state: " + str(self.state)) +# logger.info("state: " + str(self.state)) # # time.sleep(2) # def resetValues(self): @@ -140,7 +139,7 @@ class BookGrabber(QThread): # for entry in iterdata: # # print(entry) # signature = str(entry) -# self.logger.log_info("Processing entry: " + signature) +# logger.info("Processing entry: " + signature) # webdata = WebRequest().get_ppn(entry).get_data() # if webdata == "error": @@ -153,19 +152,19 @@ class BookGrabber(QThread): # self.db.addBookToDatabase(bd, self.app_id, self.prof_id) # # get latest book id # self.book_id = self.db.getLastBookId() -# self.logger.log_info("Added book to database") +# logger.info("Added book to database") # state = 0 # # print(len(rds.items)) # for rds_item in rds.items: # sign = rds_item.superlocation # loc = rds_item.location -# # ic(sign, loc) -# # ic(rds_item) +# # logger.debug(sign, loc) +# # logger.debug(rds_item) # if self.app_id in sign or self.app_id in loc: # state = 1 # break -# self.logger.log_info(f"State of {signature}: {state}") +# logger.info(f"State of {signature}: {state}") # # print( # "updating availability of " # + str(self.book_id) @@ -175,12 +174,12 @@ class BookGrabber(QThread): # try: # self.db.setAvailability(self.book_id, state) # except sqlite3.OperationalError as e: -# self.logger.log_error(f"Failed to update availability: {e}") +# logger.error(f"Failed to update availability: {e}") # # time.sleep(5) # item += 1 # self.updateSignal.emit(item, len(self.data)) -# self.logger.log_info("Worker thread finished") +# logger.info("Worker thread finished") # # self.done.emit() # self.stop() # if not self.is_Running: diff --git a/src/logic/threads_autoadder.py b/src/logic/threads_autoadder.py index 231d131..3e0fe40 100644 --- a/src/logic/threads_autoadder.py +++ b/src/logic/threads_autoadder.py @@ -5,7 +5,7 @@ from PyQt6.QtCore import QThread from PyQt6.QtCore import pyqtSignal as Signal from src.backend.database import Database -from src.logic.log import MyLogger + # from src.transformers import RDS_AVAIL_DATA @@ -18,7 +18,6 @@ class AutoAdder(QThread): def __init__(self, data=None, app_id=None, prof_id=None, parent=None): super().__init__(parent) - self.logger = MyLogger("AutoAdder") self.data = data self.app_id = app_id self.prof_id = prof_id @@ -29,7 +28,7 @@ class AutoAdder(QThread): def run(self): self.db = Database() # show the dialog, start the thread to gather data and dynamically update progressbar and listwidget - self.logger.log_info("Starting worker thread") + logger.info("Starting worker thread") item = 0 for entry in self.data: try: @@ -42,11 +41,11 @@ class AutoAdder(QThread): except Exception as e: # print(e) - self.logger.log_exception( + logger.exception( f"The query failed with message {e} for signature {entry}" ) continue if item == len(self.data): - self.logger.log_info("Worker thread finished") + logger.info("Worker thread finished") # teminate thread self.finished.emit() diff --git a/src/logic/threads_availchecker.py b/src/logic/threads_availchecker.py index 020c78f..bc981e9 100644 --- a/src/logic/threads_availchecker.py +++ b/src/logic/threads_availchecker.py @@ -5,7 +5,7 @@ from PyQt6.QtCore import QThread from PyQt6.QtCore import pyqtSignal as Signal from src.backend.database import Database -from src.logic.log import MyLogger + from src.logic.webrequest import BibTextTransformer, WebRequest # from src.transformers import RDS_AVAIL_DATA @@ -21,9 +21,8 @@ class AvailChecker(QThread): if links is None: links = [] super().__init__(parent) - self.logger = MyLogger("AvailChecker") - self.logger.log_info("Starting worker thread") - self.logger.log_info( + logger.info("Starting worker thread") + logger.info( "Checking availability for " + str(links) + " with appnumber " @@ -33,7 +32,7 @@ class AvailChecker(QThread): self.links = links self.appnumber = appnumber self.books = books - self.logger.log_info( + logger.info( f"Started worker with appnumber: {self.appnumber} and links: {self.links} and {len(self.books)} books..." ) time.sleep(2) @@ -43,7 +42,7 @@ class AvailChecker(QThread): state = 0 count = 0 for link in self.links: - self.logger.log_info("Processing entry: " + str(link)) + logger.info("Processing entry: " + str(link)) data = WebRequest().set_apparat(self.appnumber).get_ppn(link).get_data() transformer = BibTextTransformer("RDS") rds = transformer.get_data(data).return_data("rds_availability") @@ -60,14 +59,14 @@ class AvailChecker(QThread): if book["bookdata"].signature == link: book_id = book["id"] break - self.logger.log_info(f"State of {link}: " + str(state)) + logger.info(f"State of {link}: " + str(state)) # print("Updating availability of " + str(book_id) + " to " + str(state)) self.db.setAvailability(book_id, state) count += 1 self.updateProgress.emit(count, len(self.links)) self.updateSignal.emit(item.callnumber, state) - self.logger.log_info("Worker thread finished") + logger.info("Worker thread finished") # teminate thread self.quit() diff --git a/src/logic/webrequest.py b/src/logic/webrequest.py index 516bc52..328b07c 100644 --- a/src/logic/webrequest.py +++ b/src/logic/webrequest.py @@ -5,10 +5,9 @@ from bs4 import BeautifulSoup from ratelimit import limits, sleep_and_retry from src.logic.dataclass import BookData -from src.logic.log import MyLogger + from src.transformers import ARRAYData, BibTeXData, COinSData, RDSData, RISData -logger = MyLogger(__name__) API_URL = "https://rds.ibs-bw.de/phfreiburg/opac/RDSIndexrecord/{}/" PPN_URL = "https://rds.ibs-bw.de/phfreiburg/opac/RDSIndex/Search?type0%5B%5D=allfields&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=au&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=ti&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=ct&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=isn&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=ta&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=co&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=py&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=pp&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=pu&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=si&lookfor0%5B%5D={}&join=AND&bool0%5B%5D=AND&type0%5B%5D=zr&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND&type0%5B%5D=cc&lookfor0%5B%5D=&join=AND&bool0%5B%5D=AND" @@ -37,19 +36,19 @@ class WebRequest: self.ppn = None self.data = None self.timeout = 5 - logger.log_info("Initialized WebRequest") + logger.info("Initialized WebRequest") @property def use_any_book(self): """use any book that matches the search term""" self.use_any = True - logger.log_info("Using any book") + logger.info("Using any book") return self def set_apparat(self, apparat): self.apparat = apparat if int(self.apparat) < 10: self.apparat = f"0{self.apparat}" - logger.log_info(f"Set apparat to {self.apparat}") + logger.info(f"Set apparat to {self.apparat}") return self def get_ppn(self, signature): @@ -81,7 +80,7 @@ class WebRequest: response = requests.get(link, timeout=self.timeout) return response.text except requests.exceptions.RequestException as e: - logger.log_error(f"Request failed: {e}") + logger.error(f"Request failed: {e}") return None def get_data(self): links = self.get_book_links(self.ppn) @@ -105,7 +104,7 @@ class WebRequest: return_data.append(data) return return_data else: - logger.log_error("No
 tag found")
+                            logger.error("No 
 tag found")
                             raise ValueError("No 
 tag found")
                     if f"Semesterapparat-{self.apparat}" in item_location:
                         pre_tag = soup.find_all("pre")
@@ -116,7 +115,7 @@ class WebRequest:
                                 return_data.append(data)
                             return return_data
                         else:
-                            logger.log_error("No 
 tag found")
+                            logger.error("No 
 tag found")
                             return return_data
 
     def get_data_elsa(self):
@@ -137,7 +136,7 @@ class WebRequest:
                             return_data.append(data)
                         return return_data
                     else:
-                        logger.log_error("No 
 tag found")
+                        logger.error("No 
 tag found")
                         return return_data
 
 
@@ -155,7 +154,7 @@ class BibTextTransformer:
         self.field = None
         self.signature = None
         if mode not in self.valid_modes:
-            logger.log_error(f"Mode {mode} not valid")
+            logger.error(f"Mode {mode} not valid")
             raise ValueError(f"Mode {mode} not valid")
         self.data = None
         # self.bookdata = BookData(**self.data)
diff --git a/src/logic/wordparser.py b/src/logic/wordparser.py
index 01be787..3054fa1 100644
--- a/src/logic/wordparser.py
+++ b/src/logic/wordparser.py
@@ -1,3 +1,4 @@
+
 import pandas as pd
 from docx import Document
 import re
diff --git a/src/logic/zotero.py b/src/logic/zotero.py
index 33173b5..d340dcb 100644
--- a/src/logic/zotero.py
+++ b/src/logic/zotero.py
@@ -1,3 +1,4 @@
+
 from pyzotero import zotero
 from dataclasses import dataclass
 from src.logic.webrequest import WebRequest, BibTextTransformer
diff --git a/src/transformers/schemas.py b/src/transformers/schemas.py
new file mode 100644
index 0000000..cc479ea
--- /dev/null
+++ b/src/transformers/schemas.py
@@ -0,0 +1,122 @@
+from __future__ import annotations
+
+from typing import Optional, Any, List
+from dataclasses import dataclass
+from dataclasses import field as dataclass_field
+import json
+
+
+@dataclass
+class Item:
+    superlocation: str | None = dataclass_field(default_factory=str)
+    status: str | None = dataclass_field(default_factory=str)
+    availability: str | None = dataclass_field(default_factory=str)
+    notes: str | None = dataclass_field(default_factory=str)
+    limitation: str | None = dataclass_field(default_factory=str)
+    duedate: str | None = dataclass_field(default_factory=str)
+    id: str | None = dataclass_field(default_factory=str)
+    item_id: str | None = dataclass_field(default_factory=str)
+    ilslink: str | None = dataclass_field(default_factory=str)
+    number: int | None = dataclass_field(default_factory=int)
+    barcode: str | None = dataclass_field(default_factory=str)
+    reserve: str | None = dataclass_field(default_factory=str)
+    callnumber: str | None = dataclass_field(default_factory=str)
+    department: str | None = dataclass_field(default_factory=str)
+    locationhref: str | None = dataclass_field(default_factory=str)
+    location: str | None = dataclass_field(default_factory=str)
+
+    def from_dict(self, data: dict):
+        """Import data from dict"""
+        data = data["items"]
+        for entry in data:
+            for key, value in entry.items():
+                setattr(self, key, value)
+        return self
+
+
+@dataclass
+class RDS_AVAIL_DATA:
+    """Class to store RDS availability data"""
+
+    library_sigil: str = dataclass_field(default_factory=str)
+    items: List[Item] = dataclass_field(default_factory=list)
+
+    def import_from_dict(self, data: str):
+        """Import data from dict"""
+        edata = json.loads(data)
+        # library sigil is first key
+
+        self.library_sigil = str(list(edata.keys())[0])
+        # get data from first key
+        edata = edata[self.library_sigil]
+        for location in edata:
+            item = Item(superlocation=location).from_dict(edata[location])
+
+            self.items.append(item)
+        return self
+
+
+@dataclass
+class RDS_DATA:
+    """Class to store RDS data"""
+
+    RDS_SIGNATURE: str = dataclass_field(default_factory=str)
+    RDS_STATUS: str = dataclass_field(default_factory=str)
+    RDS_LOCATION: str = dataclass_field(default_factory=str)
+    RDS_URL: Any = dataclass_field(default_factory=str)
+    RDS_HINT: Any = dataclass_field(default_factory=str)
+    RDS_COMMENT: Any = dataclass_field(default_factory=str)
+    RDS_HOLDING: Any = dataclass_field(default_factory=str)
+    RDS_HOLDING_LEAK: Any = dataclass_field(default_factory=str)
+    RDS_INTERN: Any = dataclass_field(default_factory=str)
+    RDS_PROVENIENCE: Any = dataclass_field(default_factory=str)
+    RDS_LOCAL_NOTATION: str = dataclass_field(default_factory=str)
+    RDS_LEA: Any = dataclass_field(default_factory=str)
+
+    def import_from_dict(self, data: dict) -> RDS_DATA:
+        """Import data from dict"""
+        for key, value in data.items():
+            setattr(self, key, value)
+        return self
+
+
+@dataclass
+class RDS_GENERIC_DATA:
+    LibrarySigil: str = dataclass_field(default_factory=str)
+    RDS_DATA: List[RDS_DATA] = dataclass_field(default_factory=list)
+
+    def import_from_dict(self, data: str) -> RDS_GENERIC_DATA:
+        """Import data from dict"""
+        edata = json.loads(data)
+        # library sigil is first key
+        self.LibrarySigil = str(list(edata.keys())[0])
+        # get data from first key
+        edata = edata[self.LibrarySigil]
+        for entry in edata:
+            rds_data = RDS_DATA()  # Create a new RDS_DATA instance
+            # Populate the RDS_DATA instance from the entry
+            # This assumes that the entry is a dictionary that matches the structure of the RDS_DATA class
+            rds_data.import_from_dict(entry)
+            self.RDS_DATA.append(rds_data)  # Add the RDS_DATA instance to the list
+        return self
+
+
+@dataclass
+class LoksatzData:
+    type: Optional[str] = None
+    adis_idn: Optional[str] = None
+    t_idn: Optional[str] = None
+    ktrl_nr: Optional[str] = None
+    adis_isil: Optional[str] = None
+    adis_sigel: Optional[str] = None
+    bib_sigel: Optional[str] = None
+    standort: Optional[str] = None
+    signatur: Optional[str] = None
+    ausleihcode: Optional[str] = None
+    sig_katalog: Optional[str] = None
+    erwerb_datum: Optional[str] = None
+    medientypcode: Optional[str] = None
+    bestellart: Optional[str] = None
+    faecherstatistik: Optional[str] = None
+    exemplar_stat: Optional[str] = None
+    so_standort: Optional[str] = None
diff --git a/src/transformers/transformers.py b/src/transformers/transformers.py
index 28a2963..b824541 100644
--- a/src/transformers/transformers.py
+++ b/src/transformers/transformers.py
@@ -8,10 +8,8 @@ from typing import Any, List, Optional
 
 from pydantic import BaseModel, Field
 
-from src.logic.dataclass import BookData
-from src.logic.log import MyLogger
 
-logger = MyLogger("transformers.py")
+from src.logic.dataclass import BookData
 
 
 ###Pydatnic models
@@ -135,7 +133,7 @@ class ARRAYData:
 
             except Exception:
                 # # print(f"ARRAYData.transform failed, {source}, {search}")
-                logger.log_exception(f"ARRAYData.transform failed, no string {search}")
+                logger.exception(f"ARRAYData.transform failed, no string {search}")
                 return ""
 
         def _get_list_entry(source: str, search: str, entry: str) -> str:
diff --git a/src/ui/__init__.py b/src/ui/__init__.py
index b3aa330..7c6271d 100644
--- a/src/ui/__init__.py
+++ b/src/ui/__init__.py
@@ -1,3 +1,4 @@
+
 import pathlib
 
 from .Ui_semesterapparat_ui import Ui_MainWindow as Ui_Semesterapparat
diff --git a/src/ui/dialogs/__init__.py b/src/ui/dialogs/__init__.py
index bd5d65e..8271470 100644
--- a/src/ui/dialogs/__init__.py
+++ b/src/ui/dialogs/__init__.py
@@ -1,3 +1,4 @@
+
 from .bookdata import BookDataUI as edit_bookdata_ui
 from .login import LoginDialog as login_ui
 from .mail import Mail_Dialog
diff --git a/src/ui/dialogs/about.py b/src/ui/dialogs/about.py
index 49feff8..c467fbd 100644
--- a/src/ui/dialogs/about.py
+++ b/src/ui/dialogs/about.py
@@ -1,3 +1,4 @@
+
 from .dialog_sources.Ui_about import Ui_about
 from PyQt6 import QtWidgets
 from PyQt6.QtCore import PYQT_VERSION_STR
diff --git a/src/ui/dialogs/app_ext.py b/src/ui/dialogs/app_ext.py
index 6947b4b..da2adbd 100644
--- a/src/ui/dialogs/app_ext.py
+++ b/src/ui/dialogs/app_ext.py
@@ -1,7 +1,8 @@
+
 from PyQt6 import QtWidgets
 from .dialog_sources.Ui_apparat_extend import Ui_Dialog
 from src import Icon
-from src.backend import generateSemesterByDate, generateSemesterByOffset
+from src.backend import Semester
 class ApparatExtendDialog(QtWidgets.QDialog, Ui_Dialog):
     def __init__(
         self,
diff --git a/src/ui/dialogs/bookdata.py b/src/ui/dialogs/bookdata.py
index 82ce70a..bd90a77 100644
--- a/src/ui/dialogs/bookdata.py
+++ b/src/ui/dialogs/bookdata.py
@@ -1,3 +1,4 @@
+
 from PyQt6 import QtCore, QtWidgets
 
 from src.logic.dataclass import BookData
diff --git a/src/ui/dialogs/confirm_extend.py b/src/ui/dialogs/confirm_extend.py
index 7fdd74e..261fca0 100644
--- a/src/ui/dialogs/confirm_extend.py
+++ b/src/ui/dialogs/confirm_extend.py
@@ -1,3 +1,4 @@
+
 from .dialog_sources.Ui_confirm_extend import Ui_extend_confirm
 from PyQt6 import QtWidgets
 
diff --git a/src/ui/dialogs/dialog_sources/Ui_about.py b/src/ui/dialogs/dialog_sources/Ui_about.py
index e151ec1..bc74e77 100644
--- a/src/ui/dialogs/dialog_sources/Ui_about.py
+++ b/src/ui/dialogs/dialog_sources/Ui_about.py
@@ -1,3 +1,4 @@
+
 # Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\about.ui'
 #
 # Created by: PyQt6 UI code generator 6.6.1
diff --git a/src/ui/dialogs/dialog_sources/Ui_app_status.py b/src/ui/dialogs/dialog_sources/Ui_app_status.py
index 8fa232e..180beb1 100644
--- a/src/ui/dialogs/dialog_sources/Ui_app_status.py
+++ b/src/ui/dialogs/dialog_sources/Ui_app_status.py
@@ -1,3 +1,4 @@
+
 # Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\app_status.ui'
 #
 # Created by: PyQt6 UI code generator 6.6.1
diff --git a/src/ui/dialogs/dialog_sources/Ui_apparat_extend.py b/src/ui/dialogs/dialog_sources/Ui_apparat_extend.py
index f640967..78fc4b0 100644
--- a/src/ui/dialogs/dialog_sources/Ui_apparat_extend.py
+++ b/src/ui/dialogs/dialog_sources/Ui_apparat_extend.py
@@ -1,3 +1,4 @@
+
 # Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\apparat_extend.ui'
 #
 # Created by: PyQt6 UI code generator 6.7.1
diff --git a/src/ui/dialogs/dialog_sources/Ui_confirm_extend.py b/src/ui/dialogs/dialog_sources/Ui_confirm_extend.py
index 6d42379..3ecd937 100644
--- a/src/ui/dialogs/dialog_sources/Ui_confirm_extend.py
+++ b/src/ui/dialogs/dialog_sources/Ui_confirm_extend.py
@@ -1,3 +1,4 @@
+
 # Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\confirm_extend.ui'
 #
 # Created by: PyQt6 UI code generator 6.7.1
diff --git a/src/ui/dialogs/dialog_sources/Ui_edit_bookdata.py b/src/ui/dialogs/dialog_sources/Ui_edit_bookdata.py
index 1ed3024..0da8be9 100644
--- a/src/ui/dialogs/dialog_sources/Ui_edit_bookdata.py
+++ b/src/ui/dialogs/dialog_sources/Ui_edit_bookdata.py
@@ -1,3 +1,4 @@
+
 # Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\edit_bookdata.ui'
 #
 # Created by: PyQt6 UI code generator 6.6.1
diff --git a/src/ui/dialogs/dialog_sources/Ui_elsa_add_table_entry.py b/src/ui/dialogs/dialog_sources/Ui_elsa_add_table_entry.py
index 7f4fc1f..fb2e8b3 100644
--- a/src/ui/dialogs/dialog_sources/Ui_elsa_add_table_entry.py
+++ b/src/ui/dialogs/dialog_sources/Ui_elsa_add_table_entry.py
@@ -1,3 +1,4 @@
+
 # Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\elsa_add_table_entry.ui'
 #
 # Created by: PyQt6 UI code generator 6.7.1
diff --git a/src/ui/dialogs/dialog_sources/Ui_elsa_generate_citation.py b/src/ui/dialogs/dialog_sources/Ui_elsa_generate_citation.py
index 8f31646..4706889 100644
--- a/src/ui/dialogs/dialog_sources/Ui_elsa_generate_citation.py
+++ b/src/ui/dialogs/dialog_sources/Ui_elsa_generate_citation.py
@@ -1,3 +1,4 @@
+
 # Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\elsa_generate_citation.ui'
 #
 # Created by: PyQt6 UI code generator 6.6.1
diff --git a/src/ui/dialogs/dialog_sources/Ui_elsa_generator_confirm.py b/src/ui/dialogs/dialog_sources/Ui_elsa_generator_confirm.py
index e073252..d7f21f0 100644
--- a/src/ui/dialogs/dialog_sources/Ui_elsa_generator_confirm.py
+++ b/src/ui/dialogs/dialog_sources/Ui_elsa_generator_confirm.py
@@ -1,3 +1,4 @@
+
 # Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\elsa_generator_confirm.ui'
 #
 # Created by: PyQt6 UI code generator 6.6.1
diff --git a/src/ui/dialogs/dialog_sources/Ui_login.py b/src/ui/dialogs/dialog_sources/Ui_login.py
index b2421eb..4904ee6 100644
--- a/src/ui/dialogs/dialog_sources/Ui_login.py
+++ b/src/ui/dialogs/dialog_sources/Ui_login.py
@@ -1,3 +1,4 @@
+
 # Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\login.ui'
 #
 # Created by: PyQt6 UI code generator 6.6.1
diff --git a/src/ui/dialogs/dialog_sources/Ui_mail_preview.py b/src/ui/dialogs/dialog_sources/Ui_mail_preview.py
index 2df1d4f..7522526 100644
--- a/src/ui/dialogs/dialog_sources/Ui_mail_preview.py
+++ b/src/ui/dialogs/dialog_sources/Ui_mail_preview.py
@@ -1,3 +1,4 @@
+
 # Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\mail_preview.ui'
 #
 # Created by: PyQt6 UI code generator 6.7.1
diff --git a/src/ui/dialogs/dialog_sources/Ui_medianadder.py b/src/ui/dialogs/dialog_sources/Ui_medianadder.py
index 205212c..265c0bc 100644
--- a/src/ui/dialogs/dialog_sources/Ui_medianadder.py
+++ b/src/ui/dialogs/dialog_sources/Ui_medianadder.py
@@ -1,3 +1,4 @@
+
 # Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\medianadder.ui'
 #
 # Created by: PyQt6 UI code generator 6.7.1
diff --git a/src/ui/dialogs/dialog_sources/Ui_newMailTemplateDesigner.py b/src/ui/dialogs/dialog_sources/Ui_newMailTemplateDesigner.py
index 35b0d71..133b95d 100644
--- a/src/ui/dialogs/dialog_sources/Ui_newMailTemplateDesigner.py
+++ b/src/ui/dialogs/dialog_sources/Ui_newMailTemplateDesigner.py
@@ -1,3 +1,4 @@
+
 # Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\newMailTemplateDesigner.ui'
 #
 # Created by: PyQt6 UI code generator 6.6.1
diff --git a/src/ui/dialogs/dialog_sources/Ui_parsed_titles.py b/src/ui/dialogs/dialog_sources/Ui_parsed_titles.py
index c76f3bc..e35e2c6 100644
--- a/src/ui/dialogs/dialog_sources/Ui_parsed_titles.py
+++ b/src/ui/dialogs/dialog_sources/Ui_parsed_titles.py
@@ -1,3 +1,4 @@
+
 # Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\parsed_titles.ui'
 #
 # Created by: PyQt6 UI code generator 6.6.1
diff --git a/src/ui/dialogs/dialog_sources/Ui_reminder.py b/src/ui/dialogs/dialog_sources/Ui_reminder.py
index f58a243..02577c0 100644
--- a/src/ui/dialogs/dialog_sources/Ui_reminder.py
+++ b/src/ui/dialogs/dialog_sources/Ui_reminder.py
@@ -1,3 +1,4 @@
+
 # Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\reminder.ui'
 #
 # Created by: PyQt6 UI code generator 6.6.1
diff --git a/src/ui/dialogs/dialog_sources/Ui_settings.py b/src/ui/dialogs/dialog_sources/Ui_settings.py
index 4e266e8..110d604 100644
--- a/src/ui/dialogs/dialog_sources/Ui_settings.py
+++ b/src/ui/dialogs/dialog_sources/Ui_settings.py
@@ -1,3 +1,4 @@
+
 # Form implementation generated from reading ui file 'c:\Users\aky547\GitHub\SemesterapparatsManager\src\ui\dialogs\dialog_sources\settings.ui'
 #
 # Created by: PyQt6 UI code generator 6.7.1
diff --git a/src/ui/dialogs/elsa_add_entry.py b/src/ui/dialogs/elsa_add_entry.py
index 9b5a8e4..b76b963 100644
--- a/src/ui/dialogs/elsa_add_entry.py
+++ b/src/ui/dialogs/elsa_add_entry.py
@@ -1,10 +1,10 @@
+
 from .dialog_sources.Ui_elsa_add_table_entry import Ui_Dialog
 from src.logic.webrequest import WebRequest, BibTextTransformer
 from src import Icon
 from PyQt6 import QtWidgets
 from src.transformers.transformers import DictToTable
 from src.logic.zotero import ZoteroController
-from icecream import ic
 
 zot = ZoteroController()
 dtt = DictToTable()
@@ -117,7 +117,7 @@ class ElsaAddEntry(QtWidgets.QDialog, Ui_Dialog):
         if table["type"] == "zs":
             book = zot.createBook(table["isbn"])
             res_key = zot.createJournalArticle(book, table)
-            ic(book)
+            logger.debug(book)
             a_lastname = table["section_author"].split(";")[0].strip().split(",")[0]
             a_firstname = table["section_author"].split(";")[0].strip().split(",")[1]
             author = f"{a_lastname}, {a_firstname[0]}"
diff --git a/src/ui/dialogs/elsa_citation.py b/src/ui/dialogs/elsa_citation.py
index eae7288..710e35f 100644
--- a/src/ui/dialogs/elsa_citation.py
+++ b/src/ui/dialogs/elsa_citation.py
@@ -1,3 +1,4 @@
+
 from .dialog_sources.Ui_elsa_generate_citation import Ui_Dialog
 from PyQt6 import QtWidgets
 
diff --git a/src/ui/dialogs/elsa_gen_confirm.py b/src/ui/dialogs/elsa_gen_confirm.py
index 1cfbd0c..5fed097 100644
--- a/src/ui/dialogs/elsa_gen_confirm.py
+++ b/src/ui/dialogs/elsa_gen_confirm.py
@@ -1,3 +1,4 @@
+
 from .dialog_sources.Ui_elsa_generator_confirm import Ui_Dialog
 from PyQt6 import QtCore, QtWidgets, QtGui
 
diff --git a/src/ui/dialogs/login.py b/src/ui/dialogs/login.py
index b3f3d0a..c589762 100644
--- a/src/ui/dialogs/login.py
+++ b/src/ui/dialogs/login.py
@@ -2,16 +2,15 @@ import hashlib
 
 from PyQt6 import QtCore, QtGui, QtWidgets
 
-from src import Icon
+from src import Icon, logger
 from src.backend.admin_console import AdminCommands
 from src.backend.database import Database
 
 from .dialog_sources.Ui_login import Ui_Dialog
-from src import MyLogger
+
 
 class LoginDialog(Ui_Dialog):
     def setupUi(self, Dialog):
-        self.log = MyLogger("Login")
         Dialog.setObjectName("Dialog")
         Dialog.resize(218, 190)
         self.dialog = Dialog
@@ -76,16 +75,20 @@ class LoginDialog(Ui_Dialog):
         if self.db.login(username, hashed_password):
             self.lresult = 1  # Indicate successful login
             self.lusername = username
+            logger.success(f"User {username} logged in.")
             self.dialog.accept()
+
         else:
             # Credentials are invalid, display a warning
             if username == "" or password == "":
+                logger.warning("Invalid username or password. Login failed.")
                 warning_dialog = QtWidgets.QMessageBox()
                 warning_dialog.setIcon(QtWidgets.QMessageBox.Icon.Warning)
                 warning_dialog.setText("Please enter a username and password.")
                 warning_dialog.setWindowTitle("Login Failed")
                 warning_dialog.exec()
             else:
+                logger.warning("Invalid username or password. Login failed.")
                 warning_dialog = QtWidgets.QMessageBox()
                 warning_dialog.setIcon(QtWidgets.QMessageBox.Icon.Warning)
                 warning_dialog.setText(
diff --git a/src/ui/dialogs/mail.py b/src/ui/dialogs/mail.py
index f80b716..303512f 100644
--- a/src/ui/dialogs/mail.py
+++ b/src/ui/dialogs/mail.py
@@ -4,14 +4,12 @@ import sys
 from PyQt6 import QtCore, QtGui, QtWidgets
 
 from src import Icon, settings as config
-from src.logic.log import MyLogger
+
 
 from .dialog_sources.Ui_mail_preview import Ui_eMailPreview as MailPreviewDialog
 from .mailTemplate import MailTemplateDialog
 
 
-logger = MyLogger("Mail")
-
 empty_signature = """