tag found")
+ return return_data
+ else:
+ item_location = location.find(
+ "div", class_="col-xs-12 col-md-7 col-lg-8 rds-dl-panel"
+ ).text.strip()
+ log.debug(f"Item location: {item_location}")
+ if self.use_any:
+ pre_tag = soup.find_all("pre")
+ if pre_tag:
+ for tag in pre_tag:
+ data = tag.text.strip()
+ return_data.append(data)
+ return return_data
+ else:
+ log.error("No tag found")
+ raise ValueError("No tag found")
+ elif f"Semesterapparat-{self.apparat}" in item_location:
+ pre_tag = soup.find_all("pre")
+ return_data = []
+ if pre_tag:
+ for tag in pre_tag:
+ data = tag.text.strip()
+ return_data.append(data)
+ return return_data
+ else:
+ log.error("No tag found")
+ return return_data
+ else:
+ log.error(
+ f"Signature {self.signature} not found in {item_location}"
+ )
+ # return_data = []
+
+ return return_data
+
+ def get_data_elsa(self) -> Optional[list[str]]:
+ links = self.get_book_links(self.ppn)
+ for link in links:
+ result = self.search(link)
+ # in result search for class col-xs-12 rds-dl RDS_LOCATION
+ # if found, return text of href
+ soup = BeautifulSoup(result, "html.parser")
+ locations = soup.find_all("div", class_="col-xs-12 rds-dl RDS_LOCATION")
+ if locations:
+ for _ in locations:
+ pre_tag = soup.find_all("pre")
+ return_data = []
+ if pre_tag:
+ for tag in pre_tag:
+ data = tag.text.strip()
+ return_data.append(data)
+ return return_data
+ else:
+ log.error("No tag found")
+ return return_data
+
+
+class BibTextTransformer:
+ """Transforms data from the web into a BibText format.
+ Valid Modes are ARRAY, COinS, BibTeX, RIS, RDS
+ Raises:
+ ValueError: Raised if mode is not in valid_modes
+ """
+
+ valid_modes = [
+ TransformerType.ARRAY,
+ TransformerType.COinS,
+ TransformerType.BibTeX,
+ TransformerType.RIS,
+ TransformerType.RDS,
+ ]
+
+ def __init__(self, mode: TransformerType = TransformerType.ARRAY) -> None:
+ self.mode = mode.value
+ self.field = None
+ self.signature = None
+ if mode not in self.valid_modes:
+ log.error(f"Mode {mode} not valid")
+ raise ValueError(f"Mode {mode} not valid")
+ self.data = None
+ # self.bookdata = BookData(**self.data)
+
+ def use_signature(self, signature: str) -> "BibTextTransformer":
+ """use the exact signature to search for the book"""
+ self.signature = signature
+ return self
+
+ def get_data(self, data: Optional[list[str]] = None) -> "BibTextTransformer":
+ RIS_IDENT = "TY -"
+ ARRAY_IDENT = "[kid]"
+ COinS_IDENT = "ctx_ver"
+ BIBTEX_IDENT = "@book"
+ RDS_IDENT = "RDS ---------------------------------- "
+
+ if data is None:
+ self.data = None
+ return self
+
+ if self.mode == "RIS":
+ for line in data:
+ if RIS_IDENT in line:
+ self.data = line
+ elif self.mode == "ARRAY":
+ for line in data:
+ if ARRAY_IDENT in line:
+ self.data = line
+ elif self.mode == "COinS":
+ for line in data:
+ if COinS_IDENT in line:
+ self.data = line
+ elif self.mode == "BibTeX":
+ for line in data:
+ if BIBTEX_IDENT in line:
+ self.data = line
+ elif self.mode == "RDS":
+ for line in data:
+ if RDS_IDENT in line:
+ self.data = line
+ return self
+
+ def return_data(
+ self, option: Any = None
+ ) -> Union[
+ Optional[BookData],
+ Optional[RDS_GENERIC_DATA],
+ Optional[RDS_AVAIL_DATA],
+ None,
+ dict[str, Union[RDS_AVAIL_DATA, RDS_GENERIC_DATA]],
+ ]:
+ """Return Data to caller.
+
+ Args:
+ option (string, optional): Option for RDS as there are two filetypes. Use rds_availability or rds_data. Anything else gives a dict of both responses. Defaults to None.
+
+ Returns:
+ BookData: a dataclass containing data about the book
+ """
+ if self.data is None:
+ return None
+ match self.mode:
+ case "ARRAY":
+ return ARRAYData(self.signature).transform(self.data)
+ case "COinS":
+ return COinSData().transform(self.data)
+ case "BibTeX":
+ return BibTeXData().transform(self.data)
+ case "RIS":
+ return RISData().transform(self.data)
+ case "RDS":
+ return RDSData().transform(self.data).return_data(option)
+ case _:
+ return None
+
+ # if self.mode == "ARRAY":
+ # return ARRAYData().transform(self.data)
+ # elif self.mode == "COinS":
+ # return COinSData().transform(self.data)
+ # elif self.mode == "BibTeX":
+ # return BibTeXData().transform(self.data)
+ # elif self.mode == "RIS":
+ # return RISData().transform(self.data)
+ # elif self.mode == "RDS":
+ # return RDSData().transform(self.data).return_data(option)
+
+
+def cover(isbn):
+ test_url = f"https://www.buchhandel.de/cover/{isbn}/{isbn}-cover-m.jpg"
+ # log.debug(test_url)
+ data = requests.get(test_url, stream=True)
+ return data.content
+
+
+def get_content(soup, css_class):
+ return soup.find("div", class_=css_class).text.strip()
+
+
+if __name__ == "__main__":
+ # log.debug("main")
+ link = "CU 8500 K64"
+ data = WebRequest(71).get_ppn(link).get_data()
+ bib = BibTextTransformer("ARRAY").get_data().return_data()
+ log.debug(bib)
diff --git a/src/services/zotero.py b/src/services/zotero.py
new file mode 100644
index 0000000..6ca2588
--- /dev/null
+++ b/src/services/zotero.py
@@ -0,0 +1,340 @@
+from dataclasses import dataclass
+from typing import Optional
+
+from pyzotero import zotero
+
+from src import settings
+from src.services.webrequest import BibTextTransformer, WebRequest
+from src.shared.logging import log
+
+
+@dataclass
+class Creator:
+ firstName: str = None
+ lastName: str = None
+ creatorType: str = "author"
+
+ def from_dict(self, data: dict) -> None:
+ for key, value in data.items():
+ setattr(self, key, value)
+
+ def from_string(self, data: str) -> "Creator":
+ if "," in data:
+ self.firstName = data.split(",")[1]
+ self.lastName = data.split(",")[0]
+
+ return self
+
+ # set __dict__ object to be used in json
+
+
+@dataclass
+class Book:
+ itemType: str = "book"
+ creators: list[Creator] = None
+ tags: list = None
+ collections: list = None
+ relations: dict = None
+ title: str = None
+ abstractNote: str = None
+ series: str = None
+ seriesNumber: str = None
+ volume: str = None
+ numberOfVolumes: str = None
+ edition: str = None
+ place: str = None
+ publisher: str = None
+ date: str = None
+ numPages: str = None
+ language: str = None
+ ISBN: str = None
+ shortTitle: str = None
+ url: str = None
+ accessDate: str = None
+ archive: str = None
+ archiveLocation: str = None
+ libraryCatalog: str = None
+ callNumber: str = None
+ rights: str = None
+ extra: str = None
+
+ def to_dict(self) -> dict:
+ ret = {}
+ for key, value in self.__dict__.items():
+ if value:
+ ret[key] = value
+ return ret
+
+
+@dataclass
+class BookSection:
+ itemType: str = "bookSection"
+ title: str = None
+ creators: list[Creator] = None
+ abstractNote: str = None
+ bookTitle: str = None
+ series: str = None
+ seriesNumber: str = None
+ volume: str = None
+ numberOfVolumes: str = None
+ edition: str = None
+ place: str = None
+ publisher: str = None
+ date: str = None
+ pages: str = None
+ language: str = None
+ ISBN: str = None
+ shortTitle: str = None
+ url: str = None
+ accessDate: str = None
+ archive: str = None
+ archiveLocation: str = None
+ libraryCatalog: str = None
+ callNumber: str = None
+ rights: str = None
+ extra: str = None
+ tags = list
+ collections = list
+ relations = dict
+
+ def to_dict(self) -> dict:
+ ret = {}
+ for key, value in self.__dict__.items():
+ if value:
+ ret[key] = value
+ return ret
+
+ def assign(self, book) -> None:
+ for key, value in book.__dict__.items():
+ if key in self.__dict__.keys():
+ try:
+ setattr(self, key, value)
+ except AttributeError:
+ pass
+
+
+@dataclass
+class JournalArticle:
+ itemType = "journalArticle"
+ title: str = None
+ creators: list[Creator] = None
+ abstractNote: str = None
+ publicationTitle: str = None
+ volume: str = None
+ issue: str = None
+ pages: str = None
+ date: str = None
+ series: str = None
+ seriesTitle: str = None
+ seriesText: str = None
+ journalAbbreviation: str = None
+ language: str = None
+ DOI: str = None
+ ISSN: str = None
+ shortTitle: str = None
+ url: str = None
+ accessDate: str = None
+ archive: str = None
+ archiveLocation: str = None
+ libraryCatalog: str = None
+ callNumber: str = None
+ rights: str = None
+ extra: str = None
+ tags = list
+ collections = list
+ relations = dict
+
+ def to_dict(self) -> dict:
+ ret = {}
+ for key, value in self.__dict__.items():
+ if value:
+ ret[key] = value
+ return ret
+
+ def assign(self, book: dict) -> None:
+ for key, value in book.__dict__.items():
+ if key in self.__dict__.keys():
+ try:
+ setattr(self, key, value)
+ except AttributeError:
+ pass
+
+
+class ZoteroController:
+ zoterocfg = settings.zotero
+
+ def __init__(self):
+ if self.zoterocfg.library_id is None:
+ return
+ self.zot = zotero.Zotero( # type: ignore
+ self.zoterocfg.library_id,
+ self.zoterocfg.library_type,
+ self.zoterocfg.api_key,
+ )
+
+ def get_books(self) -> list:
+ ret = []
+ items = self.zot.top() # type: ignore
+ for item in items:
+ if item["data"]["itemType"] == "book":
+ ret.append(item)
+ return ret
+
+ # create item in zotero
+ # item is a part of a book
+ def __get_data(self, isbn) -> dict:
+ web = WebRequest()
+ web.get_ppn(isbn)
+ data = web.get_data_elsa()
+ bib = BibTextTransformer()
+ bib.get_data(data)
+ book = bib.return_data()
+ return book
+
+ # # #print(zot.item_template("bookSection"))
+ def createBook(self, isbn) -> Book:
+ book = self.__get_data(isbn)
+
+ bookdata = Book()
+ bookdata.title = book.title.split(":")[0]
+ bookdata.ISBN = book.isbn
+ bookdata.language = book.language
+ bookdata.date = book.year
+ bookdata.publisher = book.publisher
+ bookdata.url = book.link
+ bookdata.edition = book.edition
+ bookdata.place = book.place
+ bookdata.numPages = book.pages
+ authors = [
+ Creator().from_string(author).__dict__ for author in book.author.split(";")
+ ]
+ authors = [author for author in authors if author["lastName"] is not None]
+ bookdata.creators = authors
+ return bookdata
+
+ def createItem(self, item) -> Optional[str]:
+ resp = self.zot.create_items([item]) # type: ignore
+ if "successful" in resp.keys():
+ log.debug(resp)
+ return resp["successful"]["0"]["key"]
+ else:
+ return None
+
+ def deleteItem(self, key) -> None:
+ items = self.zot.items()
+ for item in items:
+ if item["key"] == key:
+ self.zot.delete_item(item) # type: ignore
+ # #print(item)
+ break
+
+ def createHGSection(self, book: Book, data: dict) -> Optional[str]:
+ log.debug(book)
+ chapter = BookSection()
+ chapter.assign(book)
+ chapter.pages = data["pages"]
+ chapter.itemType = "bookSection"
+ chapter.ISBN = ""
+ chapter.url = ""
+ chapter.title = data["chapter_title"]
+ creators = chapter.creators
+ for creator in creators:
+ creator["creatorType"] = "editor"
+ chapter.creators = creators
+ authors = [
+ Creator().from_string(author).__dict__
+ for author in data["section_author"].split(";")
+ ]
+ chapter.creators += authors
+
+ log.debug(chapter.to_dict())
+ return self.createItem(chapter.to_dict())
+ pass
+
+ def createBookSection(self, book: Book, data: dict) -> Optional[str]:
+ chapter = BookSection()
+ chapter.assign(book)
+ chapter.pages = data["pages"]
+ chapter.itemType = "bookSection"
+ chapter.ISBN = ""
+ chapter.url = ""
+ chapter.title = ""
+ return self.createItem(chapter.to_dict())
+ # chapter.creators
+
+ def createJournalArticle(self, journal, article) -> Optional[str]:
+ # #print(type(article))
+ journalarticle = JournalArticle()
+ journalarticle.assign(journal)
+ journalarticle.itemType = "journalArticle"
+ journalarticle.creators = [
+ Creator().from_string(author).__dict__
+ for author in article["section_author"].split(";")
+ ]
+ journalarticle.date = article["year"]
+ journalarticle.title = article["chapter_title"]
+ journalarticle.publicationTitle = article["work_title"].split(":")[0].strip()
+ journalarticle.pages = article["pages"]
+ journalarticle.ISSN = article["isbn"]
+ journalarticle.issue = article["issue"]
+ journalarticle.url = article["isbn"]
+
+ # #print(journalarticle.to_dict())
+
+ return self.createItem(journalarticle.to_dict())
+
+ def get_citation(self, item) -> str:
+ title = self.zot.item( # type: ignore
+ item,
+ content="bib",
+ style="deutsche-gesellschaft-fur-psychologie",
+ )[0]
+ # title = title[0]
+ title = (
+ title.replace("", "")
+ .replace("", "")
+ .replace('', "")
+ .replace("
", "")
+ .replace("&", "&")
+ )
+ return title
+
+
+if __name__ == "__main__":
+ zot = ZoteroController()
+ book = zot.createBook("DV 3000 D649 (4)")
+ row = "Döbert, Hans & Hörner, Wolfgang & Kopp, Bortho von & Reuter, Lutz R."
+ zot.createBookSection()
+
+ # book = Book()
+ # # # book.
+ # ISBN = "9783801718718"
+ # book = createBook(isbn=ISBN)
+ # chapter = BookSection()
+ # chapter.title = "Geistige Behinderung"
+ # chapter.bookTitle = book.title
+ # chapter.pages = "511 - 538"
+ # chapter.publisher = book.publisher
+ # authors = [
+ # Creator("Jennifer M.", "Phillips").__dict__,
+ # Creator("Hower", "Kwon").__dict__,
+ # Creator("Carl", "Feinstein").__dict__,
+ # Creator("Inco", "Spintczok von Brisinski").__dict__,
+ # ]
+ # publishers = book.author
+ # if isinstance(publishers, str):
+ # publishers = [publishers]
+ # for publisher in publishers:
+ # # #print(publisher)
+ # creator = Creator().from_string(publisher)
+ # creator.creatorType = "editor"
+ # authors.append(creator.__dict__)
+
+ # chapter.creators = authors
+ # chapter.publisher = book.publisher
+ # # #print(chapter.to_dict())
+ # createBookSection(chapter.to_dict())
+ # get_citation("9ZXH8DDE")
+ # # # #print()
+ # # #print(get_books())
+ # # #print(zot.item_creator_types("bookSection"))
diff --git a/src/shared/__init__.py b/src/shared/__init__.py
new file mode 100644
index 0000000..fe09d94
--- /dev/null
+++ b/src/shared/__init__.py
@@ -0,0 +1,6 @@
+"""Shared utilities and cross-cutting concerns."""
+
+from .logging import log
+from .config import Settings, load_config
+
+__all__ = ["log", "Settings", "load_config"]
diff --git a/src/shared/config.py b/src/shared/config.py
new file mode 100644
index 0000000..029dd15
--- /dev/null
+++ b/src/shared/config.py
@@ -0,0 +1,66 @@
+"""Application configuration and settings."""
+
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import Any
+
+import yaml
+
+from src.shared.logging import log
+
+
+@dataclass
+class Settings:
+ """Settings for the application."""
+
+ save_path: str
+ database_name: str
+ database_path: str
+ bib_id: str = ""
+ default_apps: bool = True
+ custom_applications: list[dict[str, Any]] = field(default_factory=list)
+
+ def save_settings(self, config_path: str | Path = "config.yaml") -> None:
+ """Save the settings to the config file.
+
+ Args:
+ config_path: Path to the configuration file
+ """
+ try:
+ with open(config_path, "w") as f:
+ yaml.dump(self.__dict__, f)
+ log.info(f"Settings saved to {config_path}")
+ except Exception as e:
+ log.error(f"Failed to save settings: {e}")
+ raise
+
+ @classmethod
+ def load_settings(cls, config_path: str | Path = "config.yaml") -> dict[str, Any]:
+ """Load the settings from the config file.
+
+ Args:
+ config_path: Path to the configuration file
+
+ Returns:
+ Dictionary containing the loaded settings
+ """
+ try:
+ with open(config_path, "r") as f:
+ data = yaml.safe_load(f)
+ log.info(f"Settings loaded from {config_path}")
+ return data
+ except Exception as e:
+ log.error(f"Failed to load settings: {e}")
+ raise
+
+
+def load_config(config_path: str | Path = "config.yaml") -> dict[str, Any]:
+ """Convenience function to load configuration.
+
+ Args:
+ config_path: Path to the configuration file
+
+ Returns:
+ Dictionary containing the loaded settings
+ """
+ return Settings.load_settings(config_path)
diff --git a/src/ui/dialogs/Ui_edit_bookdata.py b/src/ui/dialogs/Ui_edit_bookdata.py
index eba44e0..f9865d6 100644
--- a/src/ui/dialogs/Ui_edit_bookdata.py
+++ b/src/ui/dialogs/Ui_edit_bookdata.py
@@ -8,7 +8,7 @@
from PySide6 import QtCore, QtGui, QtWidgets
-from src.logic.dataclass import BookData
+from src.core.models import BookData
class Ui_Dialog(object):
diff --git a/src/ui/dialogs/Ui_fileparser.py b/src/ui/dialogs/Ui_fileparser.py
index da869db..72fbb60 100644
--- a/src/ui/dialogs/Ui_fileparser.py
+++ b/src/ui/dialogs/Ui_fileparser.py
@@ -8,7 +8,7 @@
from PySide6 import QtCore, QtGui, QtWidgets
-from src.logic.webrequest import BibTextTransformer, WebRequest
+from src.services.webrequest import BibTextTransformer, WebRequest
class Ui_Dialog(object):
diff --git a/src/ui/dialogs/Ui_login.py b/src/ui/dialogs/Ui_login.py
index 7844c6b..b9089dd 100644
--- a/src/ui/dialogs/Ui_login.py
+++ b/src/ui/dialogs/Ui_login.py
@@ -10,8 +10,8 @@ import hashlib
from PySide6 import QtCore, QtWidgets
-from src.backend.admin_console import AdminCommands
-from src.backend.database import Database
+from src.admin import AdminCommands
+from src.database import Database
class Ui_Dialog(object):
diff --git a/src/ui/dialogs/bookdata.py b/src/ui/dialogs/bookdata.py
index 553a59f..0a7e34d 100644
--- a/src/ui/dialogs/bookdata.py
+++ b/src/ui/dialogs/bookdata.py
@@ -1,6 +1,6 @@
from PySide6 import QtWidgets
-from src.logic.dataclass import BookData
+from src.core.models import BookData
from .dialog_sources.edit_bookdata_ui import Ui_Dialog
diff --git a/src/ui/dialogs/deletedialog.py b/src/ui/dialogs/deletedialog.py
index 03d2e43..0c81bdf 100644
--- a/src/ui/dialogs/deletedialog.py
+++ b/src/ui/dialogs/deletedialog.py
@@ -3,7 +3,7 @@ from typing import Any
from PySide6 import QtCore, QtWidgets
from src import Icon
-from src.backend.database import Database
+from src.database import Database
from .dialog_sources.deletedialog_ui import Ui_Dialog
diff --git a/src/ui/dialogs/docuprint.py b/src/ui/dialogs/docuprint.py
index 750fc10..0339238 100644
--- a/src/ui/dialogs/docuprint.py
+++ b/src/ui/dialogs/docuprint.py
@@ -2,8 +2,8 @@ from natsort import natsorted
from PySide6 import QtWidgets
from src import Icon
-from src.backend import Database
-from src.logic import Semester
+from src.database import Database
+from src.core.models import Semester
from src.utils.richtext import SemapSchilder, SemesterDocument
from .dialog_sources.documentprint_ui import Ui_Dialog
diff --git a/src/ui/dialogs/elsa_add_entry.py b/src/ui/dialogs/elsa_add_entry.py
index b8879ac..17fc5b7 100644
--- a/src/ui/dialogs/elsa_add_entry.py
+++ b/src/ui/dialogs/elsa_add_entry.py
@@ -1,8 +1,8 @@
from PySide6 import QtWidgets
from src import Icon
-from src.logic.webrequest import BibTextTransformer, WebRequest
-from src.logic.zotero import ZoteroController
+from src.services.webrequest import BibTextTransformer, WebRequest
+from src.services.zotero import ZoteroController
from src.shared.logging import log
from src.transformers.transformers import DictToTable
diff --git a/src/ui/dialogs/fileparser.py b/src/ui/dialogs/fileparser.py
index c0c29cd..edb736f 100644
--- a/src/ui/dialogs/fileparser.py
+++ b/src/ui/dialogs/fileparser.py
@@ -1,6 +1,6 @@
from PySide6 import QtWidgets
-from src.logic.webrequest import BibTextTransformer, WebRequest
+from src.services.webrequest import BibTextTransformer, WebRequest
from .dialog_sources.Ui_fileparser import Ui_Dialog
diff --git a/src/ui/dialogs/login.py b/src/ui/dialogs/login.py
index 649cbaf..09b9d1f 100644
--- a/src/ui/dialogs/login.py
+++ b/src/ui/dialogs/login.py
@@ -5,7 +5,7 @@ import loguru
from PySide6 import QtCore, QtWidgets
from src import LOG_DIR, Icon
-from src.backend.database import Database
+from src.database import Database
from .dialog_sources.login_ui import Ui_Dialog
@@ -75,7 +75,7 @@ class LoginDialog(Ui_Dialog):
hashed_password = hashlib.sha256(password.encode()).hexdigest()
if len(self.db.getUsers()) == 0:
- from src.backend.admin_console import AdminCommands
+ from src.admin import AdminCommands
AdminCommands().create_admin()
self.lresult = 1 # Indicate successful login
diff --git a/src/ui/dialogs/newEdition.py b/src/ui/dialogs/newEdition.py
index a103690..77fe6e1 100644
--- a/src/ui/dialogs/newEdition.py
+++ b/src/ui/dialogs/newEdition.py
@@ -1,7 +1,7 @@
from PySide6 import QtCore, QtWidgets
-from src.backend.catalogue import Catalogue
-from src.backend.database import Database
+from src.services.catalogue import Catalogue
+from src.database import Database
from src.ui.dialogs.mail import Mail_Dialog
from .dialog_sources.order_neweditions_ui import Ui_Dialog
diff --git a/src/ui/dialogs/parsed_titles.py b/src/ui/dialogs/parsed_titles.py
index dca3cdd..416f38b 100644
--- a/src/ui/dialogs/parsed_titles.py
+++ b/src/ui/dialogs/parsed_titles.py
@@ -4,7 +4,7 @@ import loguru
from PySide6 import QtWidgets
from src import LOG_DIR
-from src.backend import AutoAdder
+from src.background import AutoAdder
from .dialog_sources.parsed_titles_ui import Ui_Form
diff --git a/src/ui/dialogs/progress.py b/src/ui/dialogs/progress.py
index cdeab3b..6abd9c2 100644
--- a/src/ui/dialogs/progress.py
+++ b/src/ui/dialogs/progress.py
@@ -5,9 +5,9 @@ from PySide6 import QtCore
from PySide6.QtWidgets import QDialog, QPushButton, QVBoxLayout
from qtqdm import Qtqdm, QtqdmProgressBar
-from src.logic import BookData
-from src.logic.lehmannsapi import LehmannsClient
-from src.logic.SRU import SWB
+from src.core.models import BookData
+from src.services.lehmanns import LehmannsClient
+from src.services.sru import SWB
class CheckThread(QtCore.QThread):
diff --git a/src/ui/userInterface.py b/src/ui/userInterface.py
index efac025..3bf6da8 100644
--- a/src/ui/userInterface.py
+++ b/src/ui/userInterface.py
@@ -15,24 +15,27 @@ from PySide6.QtGui import QRegularExpressionValidator
from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer
from src import Icon
-from src.backend import (
+from src.database import Database
+from src.background import (
AvailChecker,
BookGrabber,
- Database,
DocumentationThread,
NewEditionCheckerThread,
)
-from src.backend.create_file import recreateFile
-from src.backend.delete_temp_contents import delete_temp_contents as tempdelete
-from src.logic import (
- APP_NRS,
+from src.utils.files import recreateFile, delete_temp_contents as tempdelete
+from src.core.models import (
Apparat,
ApparatData,
BookData,
Prof,
SemapDocument,
Semester,
+)
+from src.core.constants import APP_NRS
+from src.parsers import (
csv_to_list,
+)
+from src.logic import (
eml_to_semap,
pdf_to_semap,
word_to_semap,
diff --git a/src/ui/widgets/MessageCalendar.py b/src/ui/widgets/MessageCalendar.py
index cc03437..4c6b89f 100644
--- a/src/ui/widgets/MessageCalendar.py
+++ b/src/ui/widgets/MessageCalendar.py
@@ -5,7 +5,7 @@ from PySide6 import QtCore, QtWidgets
from PySide6.QtCore import QDate
from PySide6.QtGui import QColor, QPen
-from src.backend import Database
+from src.database import Database
from src.shared.logging import log
color = "#ddfb00" if darkdetect.isDark() else "#2204ff"
diff --git a/src/ui/widgets/admin_create_user.py b/src/ui/widgets/admin_create_user.py
index d9a0e68..84f4a16 100644
--- a/src/ui/widgets/admin_create_user.py
+++ b/src/ui/widgets/admin_create_user.py
@@ -1,7 +1,8 @@
from PySide6 import QtWidgets
from PySide6.QtCore import Signal
from .widget_sources.admin_create_user_ui import Ui_Dialog
-from src.backend import AdminCommands, Database
+from src.admin import AdminCommands
+from src.database import Database
class UserCreate(QtWidgets.QDialog, Ui_Dialog):
diff --git a/src/ui/widgets/admin_edit_prof.py b/src/ui/widgets/admin_edit_prof.py
index 083cded..d94aa5c 100644
--- a/src/ui/widgets/admin_edit_prof.py
+++ b/src/ui/widgets/admin_edit_prof.py
@@ -4,8 +4,8 @@ import loguru
from PySide6 import QtWidgets
from src import LOG_DIR
-from src.backend import Database
-from src.logic import Prof
+from src.database import Database
+from src.core.models import Prof
from .widget_sources.admin_edit_prof_ui import Ui_Dialog #
diff --git a/src/ui/widgets/admin_edit_user.py b/src/ui/widgets/admin_edit_user.py
index bbf78ca..101911f 100644
--- a/src/ui/widgets/admin_edit_user.py
+++ b/src/ui/widgets/admin_edit_user.py
@@ -1,6 +1,7 @@
from PySide6 import QtWidgets
-from src.backend import AdminCommands, Database
+from src.admin import AdminCommands
+from src.database import Database
from .widget_sources.admin_edit_user_ui import Ui_Dialog
diff --git a/src/ui/widgets/admin_query.py b/src/ui/widgets/admin_query.py
index c4d6fe1..2c63ace 100644
--- a/src/ui/widgets/admin_query.py
+++ b/src/ui/widgets/admin_query.py
@@ -1,7 +1,7 @@
from PySide6 import QtCore, QtWidgets
from src import Icon
-from src.backend import Database
+from src.database import Database
from .widget_sources. import Ui_Form
diff --git a/src/ui/widgets/calendar_entry.py b/src/ui/widgets/calendar_entry.py
index 2dd68a1..e8327ae 100644
--- a/src/ui/widgets/calendar_entry.py
+++ b/src/ui/widgets/calendar_entry.py
@@ -2,7 +2,7 @@ from PySide6 import QtWidgets
from PySide6.QtCore import Signal
from src import Icon
-from src.backend.database import Database
+from src.database import Database
from .widget_sources.calendar_entry_ui import Ui_Dialog
diff --git a/src/ui/widgets/elsa_main.py b/src/ui/widgets/elsa_main.py
index 6607446..4975b43 100644
--- a/src/ui/widgets/elsa_main.py
+++ b/src/ui/widgets/elsa_main.py
@@ -5,8 +5,10 @@ from PySide6.QtCore import QDate
from PySide6.QtGui import QRegularExpressionValidator
from src import Icon
-from src.backend import Database, recreateElsaFile
-from src.logic import Prof, Semester, elsa_word_to_csv
+from src.database import Database
+from src.utils.files import recreateElsaFile
+from src.core.models import Prof, Semester
+from src.logic import elsa_word_to_csv
from src.shared.logging import log
from src.ui.dialogs import ElsaAddEntry, popus_confirm
from src.ui.widgets.filepicker import FilePicker
diff --git a/src/ui/widgets/graph.py b/src/ui/widgets/graph.py
index ad5d1d8..838744f 100644
--- a/src/ui/widgets/graph.py
+++ b/src/ui/widgets/graph.py
@@ -5,7 +5,7 @@ from PySide6 import QtCore, QtGui, QtWidgets
from PySide6.QtCharts import QCategoryAxis, QChart, QChartView, QLineSeries, QValueAxis
from PySide6.QtGui import QColor, QPainter, QPen
-from src.logic.semester import Semester
+from src.core.models import Semester
def mergedicts(d1: dict[str, Any], d2: dict[str, Any]):
@@ -101,7 +101,7 @@ class DataQtGraph(QtWidgets.QWidget):
self.chart.createDefaultAxes()
for entry in lst:
- # print("entry:", entry)
+ print("entry:", entry)
entryseries = QLineSeries()
for x_val, y_val in zip(entry["x"], entry["y"]):
#
diff --git a/src/ui/widgets/new_edition_check.py b/src/ui/widgets/new_edition_check.py
index 56a36b3..e903265 100644
--- a/src/ui/widgets/new_edition_check.py
+++ b/src/ui/widgets/new_edition_check.py
@@ -4,8 +4,8 @@ from PySide6 import QtWidgets
from PySide6.QtCore import Qt
from src import Icon
-from src.backend.catalogue import Catalogue
-from src.logic import BookData
+from src.services.catalogue import Catalogue
+from src.core.models import BookData
from .widget_sources.new_edition_check_book_ui import (
Ui_Dialog as Ui_NewEditionCheckBook,
diff --git a/src/ui/widgets/searchPage.py b/src/ui/widgets/searchPage.py
index c8cb0f1..c415161 100644
--- a/src/ui/widgets/searchPage.py
+++ b/src/ui/widgets/searchPage.py
@@ -4,9 +4,9 @@ from natsort import natsorted
from PySide6 import QtCore, QtGui, QtWidgets
from PySide6.QtCore import Signal
-from src.backend import Database
-from src.logic import BookData, Prof, Semester, custom_sort, sort_semesters_list
-from src.logic.dataclass import Apparat
+from src.core.models import Apparat, BookData, Prof, Semester
+from src.database import Database
+from src.logic import custom_sort, sort_semesters_list
from src.shared.logging import log
from src.ui.dialogs import ApparatExtendDialog, Mail_Dialog, ReminderDialog
from src.ui.widgets import DataQtGraph, StatusWidget
@@ -374,6 +374,7 @@ class SearchStatisticPage(QtWidgets.QDialog, Ui_Dialog):
"x": [i[0] for i in data],
"y": {"Erstellt": [i[1] for i in data], "Gelöscht": [i[2] for i in data]},
}
+ log.debug(graph_data)
graph = DataQtGraph(
title="Erstellte und gelöschte Apparate",
data=graph_data,
diff --git a/src/ui/widgets/signature_update.py b/src/ui/widgets/signature_update.py
index b0b49fe..92f4cf1 100644
--- a/src/ui/widgets/signature_update.py
+++ b/src/ui/widgets/signature_update.py
@@ -6,10 +6,10 @@ from queue import Empty, Queue
from PySide6 import QtCore, QtWidgets
from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer
-from src.backend.catalogue import Catalogue
-from src.backend.database import Database
-from src.backend.webadis import get_book_medianr
-from src.logic.SRU import SWB
+from src.services.catalogue import Catalogue
+from src.database import Database
+from src.services.webadis import get_book_medianr
+from src.services.sru import SWB
from src.shared.logging import log
from .widget_sources.admin_update_signatures_ui import Ui_Dialog
diff --git a/src/ui/widgets/welcome_wizard.py b/src/ui/widgets/welcome_wizard.py
index 85418e5..c9a1700 100644
--- a/src/ui/widgets/welcome_wizard.py
+++ b/src/ui/widgets/welcome_wizard.py
@@ -5,7 +5,7 @@ from appdirs import AppDirs
from PySide6 import QtCore, QtWidgets
from src import settings
-from src.backend import Database
+from src.database import Database
from src.shared.logging import log
from .widget_sources.welcome_wizard_ui import Ui_Wizard
@@ -80,7 +80,7 @@ class WelcomeWizard(QtWidgets.QWizard, Ui_Wizard):
self.settings_database_name.setText("semesterapparate.db")
def test_login_data(self):
- from src.backend import AdminCommands
+ from src.admin import AdminCommands
log.info("Testing login data for SAM user")
db_path = (
@@ -109,7 +109,7 @@ class WelcomeWizard(QtWidgets.QWizard, Ui_Wizard):
def create_sam_user(self):
"""Create a SAM user in the database."""
- from src.backend import AdminCommands
+ from src.admin import AdminCommands
db_path = (
self.settings_database.text() + "/" + self.settings_database_name.text()
diff --git a/src/utils/files.py b/src/utils/files.py
new file mode 100644
index 0000000..12f0f81
--- /dev/null
+++ b/src/utils/files.py
@@ -0,0 +1,100 @@
+"""File operations and management utilities."""
+
+import os
+from pathlib import Path
+
+from src import LOG_DIR, settings
+from src.database import Database
+from src.shared.logging import log
+
+
+def recreate_file(name: str, app_id: int, filetype: str, open_file: bool = True) -> Path:
+ """
+ Recreate a file from the database and optionally open it.
+
+ Args:
+ name: The filename selected by the user.
+ app_id: The ID of the apparatus.
+ filetype: The extension of the file to be created.
+ open_file: Determines if the file should be opened. Defaults to True.
+
+ Returns:
+ Absolute path to the file.
+ """
+ db = Database()
+ path = db.recreateFile(name, app_id, filetype=filetype)
+ path = Path(path)
+ log.info(f"File created: {path}")
+
+ if open_file:
+ path = path.resolve()
+ if os.getenv("OS") == "Windows_NT":
+ os.startfile(path)
+ else:
+ os.system(f"open {path}")
+
+ return path
+
+
+# Legacy name for backwards compatibility
+def recreateFile(name: str, app_id: int, filetype: str, open: bool = True) -> Path:
+ """Legacy function name - use recreate_file instead."""
+ return recreate_file(name, app_id, filetype, open)
+
+
+def recreate_elsa_file(filename: str, filetype: str, open_file: bool = True) -> Path:
+ """
+ Recreate an ELSA file from the database and optionally open it.
+
+ Args:
+ filename: The filename selected by the user.
+ filetype: The file extension.
+ open_file: Determines if the file should be opened. Defaults to True.
+
+ Returns:
+ Absolute path to the file.
+ """
+ if filename.startswith("(") and filename.endswith(")"):
+ filename = str(filename[1:-1].replace("'", ""))
+
+ if not isinstance(filename, str):
+ raise ValueError("filename must be a string")
+
+ db = Database()
+ path = db.recreateElsaFile(filename, filetype)
+ path = Path(path)
+
+ if open_file:
+ path = path.resolve()
+ if os.getenv("OS") == "Windows_NT":
+ os.startfile(path)
+ else:
+ os.system(f"open {path}")
+
+ return path
+
+
+# Legacy name for backwards compatibility
+def recreateElsaFile(filename: str, filetype: str, open: bool = True) -> Path:
+ """Legacy function name - use recreate_elsa_file instead."""
+ return recreate_elsa_file(filename, filetype, open)
+
+
+def delete_temp_contents() -> None:
+ """Delete the contents of the temp directory."""
+ database = settings.database
+ path = database.temp.expanduser()
+
+ for root, dirs, files in os.walk(path, topdown=False):
+ for file in files:
+ try:
+ os.remove(os.path.join(root, file))
+ except Exception as e:
+ log.warning(f"Could not remove file {file}: {e}")
+ for dir in dirs:
+ try:
+ os.rmdir(os.path.join(root, dir))
+ except Exception as e:
+ log.warning(f"Could not remove directory {dir}: {e}")
+
+ log.info(f"Temp directory cleared: {path}")
diff --git a/test.py b/test.py
index 5a5da36..7a1ffda 100644
--- a/test.py
+++ b/test.py
@@ -1,33 +1,15 @@
-from src.logic.semester import Semester
+from src.backend.catalogue import Catalogue
+from src.logic.webrequest import BibTextTransformer, WebRequest
-sem1 = Semester.from_string("WiSe 23/24")
-print(sem1.value)
-sem2 = Semester.from_string("SoSe 24")
-print(sem2.value)
-sem3 = Semester()
-print(sem3.value)
+cat = Catalogue()
+result = cat.get_book("3825872475")
+print(result)
+web = WebRequest()
+web.get_ppn("3825872475")
+data = web.get_data_elsa()
+print(data)
+bib = BibTextTransformer()
+bib.get_data(result)
+book = bib.return_data()
-print("Comparing Sem1 with sem2")
-assert sem1.isPastSemester(sem2) is True
-assert sem1.isFutureSemester(sem2) is False
-assert sem1.isMatch(sem2) is False
-print("Comparing Sem2 with sem1")
-assert sem2.isPastSemester(sem1) is False
-assert sem2.isFutureSemester(sem1) is True
-assert sem2.isMatch(sem1) is False
-print("Comparing Sem1 with sem1")
-assert sem1.isPastSemester(sem1) is False
-assert sem1.isFutureSemester(sem1) is False
-assert sem1.isMatch(sem1) is True
-print("Comparing Sem2 with sem2")
-assert sem2.isPastSemester(sem2) is False
-assert sem2.isFutureSemester(sem2) is False
-assert sem2.isMatch(sem2) is True
-print("Comparing Sem3 with sem3")
-assert sem3.isPastSemester(sem3) is False
-assert sem3.isFutureSemester(sem3) is False
-assert sem3.isMatch(sem3) is True
-print("Comparing Sem1 with sem3")
-assert sem1.isPastSemester(sem3) is True
-assert sem1.isFutureSemester(sem3) is False
-assert sem1.isMatch(sem3) is False
+print(book)
diff --git a/tests/test_migrations_runner.py b/tests/test_migrations_runner.py
index c86561c..2ffbcdb 100644
--- a/tests/test_migrations_runner.py
+++ b/tests/test_migrations_runner.py
@@ -1,7 +1,7 @@
import sqlite3 as sql
from pathlib import Path
-from src.backend.database import Database
+from src.database import Database
p = Path("devtests_test_migrations.db")
if p.exists():
diff --git a/uv.lock b/uv.lock
index 9ca2491..6605328 100644
--- a/uv.lock
+++ b/uv.lock
@@ -165,6 +165,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216 },
]
+[[package]]
+name = "chardet"
+version = "5.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385 },
+]
+
[[package]]
name = "charset-normalizer"
version = "3.4.3"
@@ -226,6 +235,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/f4/7b7fdbb613992013c4518a0bf8fee2915f79ec07bcfa6180569bca7fa8ef/comtypes-1.4.11-py3-none-any.whl", hash = "sha256:1760d5059ca7ca1d61b574c998378d879c271a86c41f88926619ea97497592bb", size = 246365 },
]
+[[package]]
+name = "cssselect"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/0a/c3ea9573b1dc2e151abfe88c7fe0c26d1892fe6ed02d0cdb30f0d57029d5/cssselect-1.3.0.tar.gz", hash = "sha256:57f8a99424cfab289a1b6a816a43075a4b00948c86b4dcf3ef4ee7e15f7ab0c7", size = 42870 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ee/58/257350f7db99b4ae12b614a36256d9cc870d71d9e451e79c2dc3b23d7c3c/cssselect-1.3.0-py3-none-any.whl", hash = "sha256:56d1bf3e198080cc1667e137bc51de9cadfca259f03c2d4e09037b3e01e30f0d", size = 18786 },
+]
+
[[package]]
name = "darkdetect"
version = "0.8.0"
@@ -820,6 +838,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
]
+[[package]]
+name = "pdfminer"
+version = "20191125"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycryptodome" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/a3/155c5cde5f9c0b1069043b2946a93f54a41fd72cc19c6c100f6f2f5bdc15/pdfminer-20191125.tar.gz", hash = "sha256:9e700bc731300ed5c8936343c1dd4529638184198e54e91dd2b59b64a755dc01", size = 4173248 }
+
+[[package]]
+name = "pdfquery"
+version = "0.4.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "chardet" },
+ { name = "cssselect" },
+ { name = "lxml" },
+ { name = "pdfminer" },
+ { name = "pyquery" },
+ { name = "roman" },
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e5/ed/caf087d2d65ceef10fb117af79bbab50ea3a24ed8b1dc8abb0dc8039d2d3/pdfquery-0.4.3.tar.gz", hash = "sha256:a2a2974cb312fda4f569adc8d63377d25d5c6367240b4a7bfb165392c73e1dce", size = 17489 }
+
[[package]]
name = "plaster"
version = "1.1.2"
@@ -882,6 +924,38 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810 },
]
+[[package]]
+name = "pycryptodome"
+version = "3.23.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152 },
+ { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348 },
+ { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033 },
+ { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142 },
+ { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384 },
+ { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237 },
+ { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898 },
+ { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197 },
+ { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600 },
+ { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740 },
+ { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685 },
+ { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627 },
+ { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362 },
+ { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625 },
+ { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954 },
+ { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534 },
+ { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853 },
+ { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465 },
+ { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414 },
+ { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484 },
+ { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636 },
+ { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675 },
+ { url = "https://files.pythonhosted.org/packages/9f/7c/f5b0556590e7b4e710509105e668adb55aa9470a9f0e4dea9c40a4a11ce1/pycryptodome-3.23.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:350ebc1eba1da729b35ab7627a833a1a355ee4e852d8ba0447fafe7b14504d56", size = 1705791 },
+ { url = "https://files.pythonhosted.org/packages/33/38/dcc795578d610ea1aaffef4b148b8cafcfcf4d126b1e58231ddc4e475c70/pycryptodome-3.23.0-pp27-pypy_73-win32.whl", hash = "sha256:93837e379a3e5fd2bb00302a47aee9fdf7940d83595be3915752c74033d17ca7", size = 1780265 },
+]
+
[[package]]
name = "pydantic"
version = "2.11.7"
@@ -982,6 +1056,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120 },
]
+[[package]]
+name = "pyquery"
+version = "2.0.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cssselect" },
+ { name = "lxml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ae/48/79e774ea00b671d08867f06d9258203be81834236c150ac00e942d8fc4db/pyquery-2.0.1.tar.gz", hash = "sha256:0194bb2706b12d037db12c51928fe9ebb36b72d9e719565daba5a6c595322faf", size = 44999 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/f5/5067b48012967ea166b9bd0a015b69e0560e4c6e7c06f28d9bab8f9dd10b/pyquery-2.0.1-py3-none-any.whl", hash = "sha256:aedfa0bd0eb9afc94b3ddbec8f375a6362b32bc9662f46e3e0d866483f4771b0", size = 22573 },
+]
+
[[package]]
name = "pyramid"
version = "2.0.2"
@@ -1210,6 +1297,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b6/c2/9fce4c8a9587c4e90500114d742fe8ef0fd92d7bad29d136bb9941add271/rich_click-1.8.9-py3-none-any.whl", hash = "sha256:c3fa81ed8a671a10de65a9e20abf642cfdac6fdb882db1ef465ee33919fbcfe2", size = 36082 },
]
+[[package]]
+name = "roman"
+version = "5.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/30/86/8bdb59db4b7ea9a2bd93f8d25298981e09a4c9f4744cf4cbafa7ef6fee7b/roman-5.1.tar.gz", hash = "sha256:3a86572e9bc9183e771769601189e5fa32f1620ffeceebb9eca836affb409986", size = 8066 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f7/d0/27c9840ddaf331ace898c7f4aa1e1304a7acc22b844b5420fabb6d14c3a0/roman-5.1-py3-none-any.whl", hash = "sha256:bf595d8a9bc4a8e8b1dfa23e1d4def0251b03b494786df6b8c3d3f1635ce285a", size = 5825 },
+]
+
[[package]]
name = "semesterapparatsmanager"
version = "1.0.0"
@@ -1231,6 +1327,7 @@ dependencies = [
{ name = "omegaconf" },
{ name = "openai" },
{ name = "pandas" },
+ { name = "pdfquery" },
{ name = "playwright" },
{ name = "pyramid" },
{ name = "pyside6" },
@@ -1268,6 +1365,7 @@ requires-dist = [
{ name = "omegaconf", specifier = ">=2.3.0" },
{ name = "openai", specifier = ">=1.79.0" },
{ name = "pandas", specifier = ">=2.2.3" },
+ { name = "pdfquery", specifier = ">=0.4.3" },
{ name = "playwright", specifier = ">=1.49.1" },
{ name = "pyramid", specifier = ">=2.0.2" },
{ name = "pyside6", specifier = ">=6.9.1" },