Merge pull request 'Refactor and enhance type hints across multiple modules' (#17) from dev-rebase into dev

Reviewed-on: #17
This commit was merged in pull request #17.
This commit is contained in:
2025-10-21 08:11:21 +01:00
26 changed files with 437 additions and 396 deletions

View File

@@ -3,6 +3,8 @@ __author__ = "Alexander Kirchner"
__all__ = ["__version__", "__author__", "Icon", "settings"]
import os
from pathlib import Path
from typing import Union
from appdirs import AppDirs
@@ -18,7 +20,7 @@ if not os.path.exists(CONFIG_DIR): # type: ignore
settings = Config(f"{CONFIG_DIR}/config.yaml")
DATABASE_DIR = ( # type: ignore
DATABASE_DIR: Union[Path, str] = ( # type: ignore
app.user_config_dir if settings.database.path is None else settings.database.path # type: ignore
)
if not os.path.exists(DATABASE_DIR): # type: ignore

View File

@@ -1,3 +1,5 @@
from typing import List
import regex
import requests
from bs4 import BeautifulSoup
@@ -33,13 +35,13 @@ class Catalogue:
response = requests.get(link, timeout=self.timeout)
return response.text
def get_book_links(self, searchterm: str):
def get_book_links(self, searchterm: str) -> List[str]:
response = self.search_book(searchterm)
soup = BeautifulSoup(response, "html.parser")
links = soup.find_all("a", class_="title getFull")
res = []
res: List[str] = []
for link in links:
res.append(BASE + link["href"])
res.append(BASE + link["href"]) # type: ignore
return res
def get_book(self, searchterm: str):

View File

@@ -144,7 +144,7 @@ class Database:
self.create_tables()
self.insertSubjects()
def getElsaMediaID(self, work_author, signature, pages):
def getElsaMediaID(self, work_author: str, signature: str, pages: str):
query = (
"SELECT id FROM elsa_media WHERE work_author=? AND signature=? AND pages=?"
)
@@ -160,7 +160,7 @@ class Database:
query = "SELECT type FROM elsa_media WHERE id=?"
return self.query_db(query, (id,), one=True)[0]
def get_db_contents(self) -> Union[List[Tuple], None]:
def get_db_contents(self) -> Union[List[Tuple[Any]], None]:
"""
Get the contents of the

View File

@@ -86,7 +86,7 @@ def _text(elem: Optional[ET.Element]) -> str:
return (elem.text or "") if elem is not None else ""
def _req_text(parent: ET.Element, path: str) -> str:
def _req_text(parent: ET.Element, path: str) -> Optional[str]:
el = parent.find(path, NS)
if el is None or el.text is None:
return None
@@ -98,7 +98,7 @@ def parse_marc_record(record_el: ET.Element) -> MarcRecord:
record_el is the <marc:record> element (default ns MARC in your sample)
"""
# leader
leader_text = _req_text(record_el, "marc:leader")
leader_text = _req_text(record_el, "marc:leader") or ""
# controlfields
controlfields: List[ControlField] = []
@@ -124,8 +124,8 @@ def parse_marc_record(record_el: ET.Element) -> MarcRecord:
def parse_record(zs_record_el: ET.Element) -> Record:
recordSchema = _req_text(zs_record_el, "zs:recordSchema")
recordPacking = _req_text(zs_record_el, "zs:recordPacking")
recordSchema = _req_text(zs_record_el, "zs:recordSchema") or ""
recordPacking = _req_text(zs_record_el, "zs:recordPacking") or ""
# recordData contains a MARC <record> with default MARC namespace in your sample
recordData_el = zs_record_el.find("zs:recordData", NS)
@@ -140,7 +140,7 @@ def parse_record(zs_record_el: ET.Element) -> Record:
marc_record = parse_marc_record(marc_record_el)
recordPosition = int(_req_text(zs_record_el, "zs:recordPosition"))
recordPosition = int(_req_text(zs_record_el, "zs:recordPosition") or "0")
return Record(
recordSchema=recordSchema,
recordPacking=recordPacking,

View File

@@ -1,36 +1,4 @@
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]
start_year, _ = map(int, year_part.split("/"))
return start_year, 1
else:
raise ValueError(f"Invalid semester format: {semester}")
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):
def parse_semester(semester: str) -> tuple[int, int]:
"""
Parses the semester string into a sortable format.
Returns a tuple of (year, type), where type is 0 for SoSe and 1 for WiSe.
@@ -48,6 +16,23 @@ def parse_semester(semester: str):
raise ValueError(f"Invalid semester format: {semester}")
def custom_sort(entries) -> list:
"""
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 sort_semesters_list(semesters: list) -> list:
"""
Sorts a list of semester strings based on year and type.

View File

@@ -30,184 +30,184 @@ PROF_TITLES = [
]
SEMAP_MEDIA_ACCOUNTS = {
"1": "1008000055",
"2": "1008000188",
"3": "1008000211",
"4": "1008000344",
"5": "1008000477",
"6": "1008000500",
"7": "1008000633",
"8": "1008000766",
"9": "1008000899",
"10": "1008000922",
"11": "1008001044",
"12": "1008001177",
"13": "1008001200",
"14": "1008001333",
"15": "1008001466",
"16": "1008001599",
"17": "1008001622",
"18": "1008001755",
"19": "1008001888",
"20": "1008001911",
"21": "1008002033",
"22": "1008002166",
"23": "1008002299",
"24": "1008002322",
"25": "1008002455",
"26": "1008002588",
"27": "1008002611",
"28": "1008002744",
"29": "1008002877",
"30": "1008002900",
"31": "1008003022",
"32": "1008003155",
"33": "1008003288",
"34": "1008003311",
"35": "1008003444",
"36": "1008003577",
"37": "1008003600",
"38": "1008003733",
"39": "1008003866",
"40": "1008003999",
"41": "1008004011",
"42": "1008004144",
"43": "1008004277",
"44": "1008004300",
"45": "1008004433",
"46": "1008004566",
"47": "1008004699",
"48": "1008004722",
"49": "1008004855",
"50": "1008004988",
"51": "1008005000",
"52": "1008005133",
"53": "1008005266",
"54": "1008005399",
"55": "1008005422",
"56": "1008005555",
"57": "1008005688",
"58": "1008005711",
"59": "1008005844",
"60": "1008005977",
"61": "1008006099",
"62": "1008006122",
"63": "1008006255",
"64": "1008006388",
"65": "1008006411",
"66": "1008006544",
"67": "1008006677",
"68": "1008006700",
"69": "1008006833",
"70": "1008006966",
"71": "1008007088",
"72": "1008007111",
"73": "1008007244",
"74": "1008007377",
"75": "1008007400",
"76": "1008007533",
"77": "1008007666",
"78": "1008007799",
"79": "1008007822",
"80": "1008007955",
"81": "1008008077",
"82": "1008008100",
"83": "1008008233",
"84": "1008008366",
"85": "1008008499",
"86": "1008008522",
"87": "1008008655",
"88": "1008008788",
"89": "1008008811",
"90": "1008008944",
"91": "1008009066",
"92": "1008009199",
"93": "1008009222",
"94": "1008009355",
"95": "1008009488",
"96": "1008009511",
"97": "1008009644",
"98": "1008009777",
"99": "1008009800",
"100": "1008009933",
"101": "1008010022",
"102": "1008010155",
"103": "1008010288",
"104": "1008010311",
"105": "1008010444",
"106": "1008010577",
"107": "1008010600",
"108": "1008010733",
"109": "1008010866",
"110": "1008010999",
"111": "1008011011",
"112": "1008011144",
"113": "1008011277",
"114": "1008011300",
"115": "1008011433",
"116": "1008011566",
"117": "1008011699",
"118": "1008011722",
"119": "1008011855",
"120": "1008011988",
"121": "1008012000",
"122": "1008012133",
"123": "1008012266",
"124": "1008012399",
"125": "1008012422",
"126": "1008012555",
"127": "1008012688",
"128": "1008012711",
"129": "1008012844",
"130": "1008012977",
"131": "1008013099",
"132": "1008013122",
"133": "1008013255",
"134": "1008013388",
"135": "1008013411",
"136": "1008013544",
"137": "1008013677",
"138": "1008013700",
"139": "1008013833",
"140": "1008013966",
"141": "1008014088",
"142": "1008014111",
"143": "1008014244",
"144": "1008014377",
"145": "1008014400",
"146": "1008014533",
"147": "1008014666",
"148": "1008014799",
"149": "1008014822",
"150": "1008014955",
"151": "1008015077",
"152": "1008015100",
"153": "1008015233",
"154": "1008015366",
"155": "1008015499",
"156": "1008015522",
"157": "1008015655",
"158": "1008015788",
"159": "1008015811",
"160": "1008015944",
"161": "1008016066",
"162": "1008016199",
"163": "1008016222",
"164": "1008016355",
"165": "1008016488",
"166": "1008016511",
"167": "1008016644",
"168": "1008016777",
"169": "1008016800",
"170": "1008016933",
"171": "1008017055",
"172": "1008017188",
"173": "1008017211",
"174": "1008017344",
"175": "1008017477",
"176": "1008017500",
"177": "1008017633",
"178": "1008017766",
"179": "1008017899",
"180": "1008017922",
1: "1008000055",
2: "1008000188",
3: "1008000211",
4: "1008000344",
5: "1008000477",
6: "1008000500",
7: "1008000633",
8: "1008000766",
9: "1008000899",
10: "1008000922",
11: "1008001044",
12: "1008001177",
13: "1008001200",
14: "1008001333",
15: "1008001466",
16: "1008001599",
17: "1008001622",
18: "1008001755",
19: "1008001888",
20: "1008001911",
21: "1008002033",
22: "1008002166",
23: "1008002299",
24: "1008002322",
25: "1008002455",
26: "1008002588",
27: "1008002611",
28: "1008002744",
29: "1008002877",
30: "1008002900",
31: "1008003022",
32: "1008003155",
33: "1008003288",
34: "1008003311",
35: "1008003444",
36: "1008003577",
37: "1008003600",
38: "1008003733",
39: "1008003866",
40: "1008003999",
41: "1008004011",
42: "1008004144",
43: "1008004277",
44: "1008004300",
45: "1008004433",
46: "1008004566",
47: "1008004699",
48: "1008004722",
49: "1008004855",
50: "1008004988",
51: "1008005000",
52: "1008005133",
53: "1008005266",
54: "1008005399",
55: "1008005422",
56: "1008005555",
57: "1008005688",
58: "1008005711",
59: "1008005844",
60: "1008005977",
61: "1008006099",
62: "1008006122",
63: "1008006255",
64: "1008006388",
65: "1008006411",
66: "1008006544",
67: "1008006677",
68: "1008006700",
69: "1008006833",
70: "1008006966",
71: "1008007088",
72: "1008007111",
73: "1008007244",
74: "1008007377",
75: "1008007400",
76: "1008007533",
77: "1008007666",
78: "1008007799",
79: "1008007822",
80: "1008007955",
81: "1008008077",
82: "1008008100",
83: "1008008233",
84: "1008008366",
85: "1008008499",
86: "1008008522",
87: "1008008655",
88: "1008008788",
89: "1008008811",
90: "1008008944",
91: "1008009066",
92: "1008009199",
93: "1008009222",
94: "1008009355",
95: "1008009488",
96: "1008009511",
97: "1008009644",
98: "1008009777",
99: "1008009800",
100: "1008009933",
101: "1008010022",
102: "1008010155",
103: "1008010288",
104: "1008010311",
105: "1008010444",
106: "1008010577",
107: "1008010600",
108: "1008010733",
109: "1008010866",
110: "1008010999",
111: "1008011011",
112: "1008011144",
113: "1008011277",
114: "1008011300",
115: "1008011433",
116: "1008011566",
117: "1008011699",
118: "1008011722",
119: "1008011855",
120: "1008011988",
121: "1008012000",
122: "1008012133",
123: "1008012266",
124: "1008012399",
125: "1008012422",
126: "1008012555",
127: "1008012688",
128: "1008012711",
129: "1008012844",
130: "1008012977",
131: "1008013099",
132: "1008013122",
133: "1008013255",
134: "1008013388",
135: "1008013411",
136: "1008013544",
137: "1008013677",
138: "1008013700",
139: "1008013833",
140: "1008013966",
141: "1008014088",
142: "1008014111",
143: "1008014244",
144: "1008014377",
145: "1008014400",
146: "1008014533",
147: "1008014666",
148: "1008014799",
149: "1008014822",
150: "1008014955",
151: "1008015077",
152: "1008015100",
153: "1008015233",
154: "1008015366",
155: "1008015499",
156: "1008015522",
157: "1008015655",
158: "1008015788",
159: "1008015811",
160: "1008015944",
161: "1008016066",
162: "1008016199",
163: "1008016222",
164: "1008016355",
165: "1008016488",
166: "1008016511",
167: "1008016644",
168: "1008016777",
169: "1008016800",
170: "1008016933",
171: "1008017055",
172: "1008017188",
173: "1008017211",
174: "1008017344",
175: "1008017477",
176: "1008017500",
177: "1008017633",
178: "1008017766",
179: "1008017899",
180: "1008017922",
}

View File

@@ -37,7 +37,7 @@ class Prof:
self._title = value
# add function that sets the data from a tuple
def from_tuple(self, data: tuple[Union[str, int], ...]):
def from_tuple(self, data: tuple[Union[str, int], ...]) -> "Prof":
setattr(self, "id", data[0])
setattr(self, "_title", data[1])
setattr(self, "firstname", data[2])
@@ -222,6 +222,7 @@ class Subjects(Enum):
for i in cls:
if i.name == name:
return i.id - 1
return None
@dataclass

View File

@@ -134,10 +134,10 @@ class LehmannsClient:
enriched.append(r)
continue
soup = BeautifulSoup(html, "html.parser")
soup = BeautifulSoup(html, "html.parser") # type: ignore
# Pages
pages_node = soup.select_one(
pages_node = soup.select_one( # type: ignore
"span.book-meta.meta-seiten[itemprop='numberOfPages'], "
"span.book-meta.meta-seiten[itemprop='numberofpages'], "
".meta-seiten [itemprop='numberOfPages'], "
@@ -151,7 +151,7 @@ class LehmannsClient:
r.pages = f"{m.group(0)} Seiten"
# Availability via li.availability-3
avail_li = soup.select_one("li.availability-3")
avail_li = soup.select_one("li.availability-3") # type: ignore
if avail_li:
avail_text = " ".join(
avail_li.get_text(" ", strip=True).split()
@@ -200,12 +200,12 @@ class LehmannsClient:
if not a:
continue
url = urljoin(BASE, a["href"].strip())
base_title = (block.select_one(".title [itemprop='name']") or a).get_text(
base_title = (block.select_one(".title [itemprop='name']") or a).get_text( # type: ignore
strip=True
)
# Alternative headline => extend title
alt_tag = block.select_one(".description[itemprop='alternativeHeadline']")
alt_tag = block.select_one(".description[itemprop='alternativeHeadline']") # type: ignore
alternative_headline = alt_tag.get_text(strip=True) if alt_tag else None
title = (
f"{base_title} : {alternative_headline}"
@@ -216,7 +216,7 @@ class LehmannsClient:
# Authors from .author
authors: list[str] = []
author_div = block.select_one("div.author")
author_div = block.select_one("div.author") # type: ignore
if author_div:
t = author_div.get_text(" ", strip=True)
t = re.sub(r"^\s*von\s+", "", t, flags=re.I)
@@ -228,7 +228,7 @@ class LehmannsClient:
# Media + format
media_type = None
book_format = None
type_text = block.select_one(".type")
type_text = block.select_one(".type") # type: ignore
if type_text:
t = type_text.get_text(" ", strip=True)
m = re.search(r"\b(Buch|eBook|Hörbuch)\b", t)
@@ -240,7 +240,7 @@ class LehmannsClient:
# Year
year = None
y = block.select_one("[itemprop='copyrightYear']")
y = block.select_one("[itemprop='copyrightYear']") # type: ignore
if y:
try:
year = int(y.get_text(strip=True))
@@ -249,7 +249,7 @@ class LehmannsClient:
# Edition
edition = None
ed = block.select_one("[itemprop='bookEdition']")
ed = block.select_one("[itemprop='bookEdition']") # type: ignore
if ed:
m = re.search(r"\d+", ed.get_text(strip=True))
if m:
@@ -257,15 +257,15 @@ class LehmannsClient:
# Publisher
publisher = None
pub = block.select_one(
pub = block.select_one( # type: ignore
".publisherprop [itemprop='name']"
) or block.select_one(".publisher [itemprop='name']")
) or block.select_one(".publisher [itemprop='name']") # type: ignore
if pub:
publisher = pub.get_text(strip=True)
# ISBN-13
isbn13 = None
isbn_tag = block.select_one(".isbn [itemprop='isbn'], [itemprop='isbn']")
isbn_tag = block.select_one(".isbn [itemprop='isbn'], [itemprop='isbn']") # type: ignore
if isbn_tag:
digits = re.sub(r"[^0-9Xx]", "", isbn_tag.get_text(strip=True))
m = re.search(r"(97[89]\d{10})", digits)
@@ -288,7 +288,7 @@ class LehmannsClient:
# Image (best-effort)
image = None
left_img = block.find_previous("img")
left_img = block.find_previous("img") # type: ignore
if left_img and left_img.get("src"):
image = urljoin(BASE, left_img["src"])

View File

@@ -1,10 +1,12 @@
from openai import OpenAI
from src import settings
import json
from typing import Any
from openai import OpenAI
from src import settings
def init_client():
def init_client() -> OpenAI:
"""Initialize the OpenAI client with the API key and model from settings."""
global client, model, api_key
if not settings.openAI.api_key:
@@ -16,9 +18,11 @@ def init_client():
api_key = settings.openAI.api_key
client = OpenAI(api_key=api_key)
return client
def run_shortener(title:str, length:int):
def run_shortener(title: str, length: int) -> list[dict[str, Any]]:
client = init_client()
response = client.responses.create(
response = client.responses.create( # type: ignore
model=model,
instructions="""you are a sentence shortener. The next message will contain the string to shorten and the length limit.
You need to shorten the string to be under the length limit, while keeping as much detail as possible. The result may NOT be longer than the length limit.
@@ -27,27 +31,28 @@ based on that, please reply only the shortened string. Give me 5 choices. if the
)
answers = response.output_text
return eval(answers) # type: ignore
#answers are strings in json format, so we need to convert them to a list of dicts
# answers are strings in json format, so we need to convert them to a list of dicts
def name_tester(name: str):
def name_tester(name: str) -> dict:
client = init_client()
response = client.responses.create(
model = model,
response = client.responses.create( # type: ignore
model=model,
instructions="""you are a name tester, You are given a name and will have to split the name into first name, last name, and if present the title. Return the name in a json format with the keys "title", "first_name", "last_name". If no title is present, set title to none. Do NOt return the answer in a codeblock, use a pure json string. Assume the names are in the usual german naming scheme""",
input = f'{{"name":"{name}"}}'
input=f'{{"name":"{name}"}}',
)
answers = response.output_text
return json.loads(answers)
def semester_converter(semester:str):
def semester_converter(semester: str) -> str:
client = init_client()
response = client.responses.create(
model = model,
response = client.responses.create( # type: ignore
model=model,
instructions="""you are a semester converter. You will be given a string. Convert this into a string like this: SoSe YY or WiSe YY/YY+1. Do not return the answer in a codeblock, use a pure string.""",
input = semester
input=semester,
)
answers = response.output_text
return answers
return answers

View File

@@ -1,10 +1,9 @@
# add depend path to system path
import pandas as pd
from pdfquery import PDFQuery
def pdf_to_csv(path: str) -> pd.DataFrame:
def pdf_to_csv(path: str) -> str:
"""
Extracts the data from a pdf file and returns it as a pandas dataframe
"""

View File

@@ -232,3 +232,17 @@ if __name__ == "__main__":
# print("generate_missing:", [str(s) for s in chain])
# Parsing demo ---------------------------------------------------------
examples = [
"SoSe 6",
"WiSe 6/7",
"WiSe 6",
"SoSe 23",
"WiSe 23/24",
"WiSe 24",
"WiSe 99/00",
"SoSe 00",
"WiSe 100/101", # test large year
]
for ex in examples:
parsed = Semester.from_string(ex)
print(f"'{ex}'{parsed} ({parsed.year=}, {parsed.semester=})")

View File

@@ -13,7 +13,7 @@ class Settings:
default_apps: bool = True
custom_applications: list[dict] = field(default_factory=list)
def save_settings(self):
def save_settings(self) -> None:
"""Save the settings to the config file."""
with open("config.yaml", "w") as f:
yaml.dump(self.__dict__, f)

View File

@@ -51,14 +51,14 @@ class WebRequest:
log.info("Using any book")
return self
def set_apparat(self, apparat: int):
def set_apparat(self, apparat: int) -> "WebRequest":
self.apparat = apparat
if int(self.apparat) < 10:
self.apparat = f"0{self.apparat}"
log.info(f"Set apparat to {self.apparat}")
return self
def get_ppn(self, signature: str):
def get_ppn(self, signature: str) -> "WebRequest":
self.signature = signature
if "+" in signature:
signature = signature.replace("+", "%2B")
@@ -90,7 +90,7 @@ class WebRequest:
@sleep_and_retry
@limits(calls=RATE_LIMIT, period=RATE_PERIOD)
def search(self, link: str):
def search(self, link: str) -> Optional[str]:
try:
response = requests.get(link, timeout=self.timeout)
return response.text
@@ -98,7 +98,7 @@ class WebRequest:
log.error(f"Request failed: {e}")
return None
def get_data(self) -> Union[list[str], None]:
def get_data(self) -> Optional[list[str]]:
links = self.get_book_links(self.ppn)
log.debug(f"Links: {links}")
return_data: list[str] = []
@@ -156,7 +156,7 @@ class WebRequest:
return return_data
def get_data_elsa(self):
def get_data_elsa(self) -> Optional[list[str]]:
links = self.get_book_links(self.ppn)
for link in links:
result = self.search(link)
@@ -197,12 +197,12 @@ class BibTextTransformer:
self.data = None
# self.bookdata = BookData(**self.data)
def use_signature(self, signature: str):
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: Union[list[str]] = None) -> "BibTextTransformer":
def get_data(self, data: Optional[list[str]] = None) -> "BibTextTransformer":
RIS_IDENT = "TY -"
ARRAY_IDENT = "[kid]"
COinS_IDENT = "ctx_ver"

View File

@@ -1,5 +1,5 @@
import zipfile
from typing import Any
from typing import Any, Optional
import fitz # PyMuPDF
import pandas as pd
@@ -35,7 +35,7 @@ def word_docx_to_csv(path: str) -> list[pd.DataFrame]:
return m_data
def get_fach(path: str) -> str:
def get_fach(path: str) -> Optional[str]:
document = zipfile.ZipFile(path)
xml_data = document.read("word/document.xml")
document.close()
@@ -49,10 +49,12 @@ def get_fach(path: str) -> str:
# get the data in the w:t
for run in para.find_all("w:r"):
data = run.find("w:t")
return data.contents[0]
if data and data.contents:
return data.contents[0]
return None
def makeDict():
def makeDict() -> dict[str, Optional[str]]:
return {
"work_author": None,
"section_author": None,
@@ -70,8 +72,8 @@ def makeDict():
}
def tuple_to_dict(tlist: tuple, type: str) -> dict:
ret = []
def tuple_to_dict(tlist: tuple, type: str) -> list[dict[str, Optional[str]]]:
ret: list[dict[str, Optional[str]]] = []
for line in tlist:
data = makeDict()
if type == "Monografien":
@@ -111,7 +113,7 @@ def tuple_to_dict(tlist: tuple, type: str) -> dict:
return ret
def elsa_word_to_csv(path: str):
def elsa_word_to_csv(path: str) -> tuple[list[dict[str, Optional[str]]], str]:
doc = Document(path)
# # print all lines in doc
doctype = [para.text for para in doc.paragraphs if para.text != ""][-1]

View File

@@ -56,8 +56,8 @@ def eml_parser(path: str) -> XMLMailSubmission:
return parse_xml_submission(xml_content)
def eml_to_semap(path: str) -> SemapDocument:
submission = eml_parser(path)
def eml_to_semap(xml_mail: XMLMailSubmission) -> SemapDocument:
submission = eml_parser(xml_mail)
semap_doc = SemapDocument(
# prof=Prof(name=submission.name, lastname=submission.lastname, email=submission.email),
apparat=Apparat(name=submission.app_name, subject=submission.subject),

View File

@@ -1,4 +1,5 @@
from dataclasses import dataclass
from typing import Optional
from pyzotero import zotero
@@ -12,11 +13,11 @@ class Creator:
lastName: str = None
creatorType: str = "author"
def from_dict(self, data: dict):
def from_dict(self, data: dict) -> None:
for key, value in data.items():
setattr(self, key, value)
def from_string(self, data: str):
def from_string(self, data: str) -> "Creator":
if "," in data:
self.firstName = data.split(",")[1]
self.lastName = data.split(",")[0]
@@ -56,7 +57,7 @@ class Book:
rights: str = None
extra: str = None
def to_dict(self):
def to_dict(self) -> dict:
ret = {}
for key, value in self.__dict__.items():
if value:
@@ -95,14 +96,14 @@ class BookSection:
collections = list
relations = dict
def to_dict(self):
def to_dict(self) -> dict:
ret = {}
for key, value in self.__dict__.items():
if value:
ret[key] = value
return ret
def assign(self, book):
def assign(self, book) -> None:
for key, value in book.__dict__.items():
if key in self.__dict__.keys():
try:
@@ -142,14 +143,14 @@ class JournalArticle:
collections = list
relations = dict
def to_dict(self):
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):
def assign(self, book: dict) -> None:
for key, value in book.__dict__.items():
if key in self.__dict__.keys():
try:
@@ -164,15 +165,15 @@ class ZoteroController:
def __init__(self):
if self.zoterocfg.library_id is None:
return
self.zot = zotero.Zotero(
self.zot = zotero.Zotero( # type: ignore
self.zoterocfg.library_id,
self.zoterocfg.library_type,
self.zoterocfg.api_key,
)
def get_books(self):
def get_books(self) -> list:
ret = []
items = self.zot.top()
items = self.zot.top() # type: ignore
for item in items:
if item["data"]["itemType"] == "book":
ret.append(item)
@@ -180,7 +181,7 @@ class ZoteroController:
# create item in zotero
# item is a part of a book
def __get_data(self, isbn):
def __get_data(self, isbn) -> dict:
web = WebRequest()
web.get_ppn(isbn)
data = web.get_data_elsa()
@@ -190,7 +191,7 @@ class ZoteroController:
return book
# # #print(zot.item_template("bookSection"))
def createBook(self, isbn):
def createBook(self, isbn) -> Book:
book = self.__get_data(isbn)
bookdata = Book()
@@ -209,23 +210,23 @@ class ZoteroController:
bookdata.creators = authors
return bookdata
def createItem(self, item):
resp = self.zot.create_items([item])
def createItem(self, item) -> Optional[str]:
resp = self.zot.create_items([item]) # type: ignore
if "successful" in resp.keys():
# #print(resp["successful"]["0"]["key"])
return resp["successful"]["0"]["key"]
else:
return None
def deleteItem(self, key):
def deleteItem(self, key) -> None:
items = self.zot.items()
for item in items:
if item["key"] == key:
self.zot.delete_item(item)
self.zot.delete_item(item) # type: ignore
# #print(item)
break
def createHGSection(self, book: Book, data: dict):
def createHGSection(self, book: Book, data: dict) -> Optional[str]:
chapter = BookSection()
chapter.assign(book)
chapter.pages = data["pages"]
@@ -247,7 +248,7 @@ class ZoteroController:
return self.createItem(chapter.to_dict())
pass
def createBookSection(self, book: Book, data: dict):
def createBookSection(self, book: Book, data: dict) -> Optional[str]:
chapter = BookSection()
chapter.assign(book)
chapter.pages = data["pages"]
@@ -258,7 +259,7 @@ class ZoteroController:
return self.createItem(chapter.to_dict())
# chapter.creators
def createJournalArticle(self, journal, article):
def createJournalArticle(self, journal, article) -> Optional[str]:
# #print(type(article))
journalarticle = JournalArticle()
journalarticle.assign(journal)
@@ -279,8 +280,8 @@ class ZoteroController:
return self.createItem(journalarticle.to_dict())
def get_citation(self, item):
title = self.zot.item(
def get_citation(self, item) -> str:
title = self.zot.item( # type: ignore
item,
content="bib",
style="deutsche-gesellschaft-fur-psychologie",

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1">
</TS>

View File

@@ -110,10 +110,10 @@ class DocumentPrintDialog(QtWidgets.QDialog, Ui_Dialog):
def on_pushButton_clicked(self):
apparats: list[tuple[int, str]] = []
apps = self.db.getAllAparats(0)
apps = natsorted(apps, key=lambda x: x[4], reverse=True)
apps = natsorted(apps, key=lambda x: x.appnr, reverse=True)
for app in apps:
prof = self.db.getProfById(app[2])
data = (app[4], f"{prof.lastname} ({app[1]})")
prof = self.db.getProfById(app.prof_id)
data = (app.appnr, f"{prof.lastname} ({app.name})")
apparats.append(data)
SemesterDocument(
semester=self.semester.value,

View File

@@ -1349,7 +1349,7 @@
</property>
<property name="toolTip">
<string>Die Apparatsdetails werden aus dem Dokument gelesen und eingetragen
Einige Angaben müssen ggf angepasst werden</string>
Die gewünschten Medien werden automatisch in die Medienliste eingetragen, evtl. unvollständig, da eBooks nicht erfasst werden könnenEinige Angaben müssen ggf angepasst werden</string>
</property>
<property name="text">
<string>Daten aus Dokument
@@ -1618,72 +1618,72 @@ Einige Angaben müssen ggf angepasst werden</string>
<attribute name="title">
<string>Admin</string>
</attribute>
<widget class="QLabel" name="label_21">
<widget class="QFrame" name="frame">
<property name="geometry">
<rect>
<x>10</x>
<y>30</y>
<width>47</width>
<height>22</height>
<x>0</x>
<y>0</y>
<width>1251</width>
<height>711</height>
</rect>
</property>
<property name="text">
<string>Aktion:</string>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
</widget>
<widget class="QComboBox" name="select_action_box">
<property name="geometry">
<rect>
<x>60</x>
<y>30</y>
<width>181</width>
<height>22</height>
</rect>
</property>
<item>
<property name="text">
<string>Nutzer anlegen</string>
</property>
</item>
<item>
<property name="text">
<string>Nutzer bearbeiten</string>
</property>
</item>
<item>
<property name="text">
<string>Lehrperson bearbeiten</string>
</property>
</item>
<item>
<property name="text">
<string>Medien bearbeiten</string>
</property>
</item>
</widget>
<widget class="QGroupBox" name="admin_action">
<property name="geometry">
<rect>
<x>10</x>
<y>70</y>
<width>570</width>
<height>291</height>
</rect>
</property>
<property name="font">
<font>
<bold>false</bold>
</font>
</property>
<property name="title">
<string>GroupBox</string>
</property>
<property name="flat">
<bool>true</bool>
</property>
<property name="checkable">
<bool>false</bool>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_21">
<property name="text">
<string>Aktion:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="select_action_box">
<item>
<property name="text">
<string>Nutzer anlegen</string>
</property>
</item>
<item>
<property name="text">
<string>Nutzer bearbeiten</string>
</property>
</item>
<item>
<property name="text">
<string>Lehrperson bearbeiten</string>
</property>
</item>
<item>
<property name="text">
<string>Medien bearbeiten</string>
</property>
</item>
</widget>
</item>
<item row="1" column="1">
<widget class="QGroupBox" name="admin_action">
<property name="font">
<font>
<bold>false</bold>
</font>
</property>
<property name="title">
<string>GroupBox</string>
</property>
<property name="flat">
<bool>true</bool>
</property>
<property name="checkable">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</widget>

View File

@@ -638,24 +638,37 @@ class Ui_MainWindow(object):
self.tabWidget.addTab(self.elsatab, "")
self.admin = QWidget()
self.admin.setObjectName(u"admin")
self.label_21 = QLabel(self.admin)
self.frame = QFrame(self.admin)
self.frame.setObjectName(u"frame")
self.frame.setGeometry(QRect(0, 0, 1251, 711))
self.frame.setFrameShape(QFrame.StyledPanel)
self.frame.setFrameShadow(QFrame.Raised)
self.formLayout_2 = QFormLayout(self.frame)
self.formLayout_2.setObjectName(u"formLayout_2")
self.label_21 = QLabel(self.frame)
self.label_21.setObjectName(u"label_21")
self.label_21.setGeometry(QRect(10, 30, 47, 22))
self.select_action_box = QComboBox(self.admin)
self.formLayout_2.setWidget(0, QFormLayout.ItemRole.LabelRole, self.label_21)
self.select_action_box = QComboBox(self.frame)
self.select_action_box.addItem("")
self.select_action_box.addItem("")
self.select_action_box.addItem("")
self.select_action_box.addItem("")
self.select_action_box.setObjectName(u"select_action_box")
self.select_action_box.setGeometry(QRect(60, 30, 181, 22))
self.admin_action = QGroupBox(self.admin)
self.formLayout_2.setWidget(0, QFormLayout.ItemRole.FieldRole, self.select_action_box)
self.admin_action = QGroupBox(self.frame)
self.admin_action.setObjectName(u"admin_action")
self.admin_action.setGeometry(QRect(10, 70, 570, 291))
font5 = QFont()
font5.setBold(False)
self.admin_action.setFont(font5)
self.admin_action.setFlat(True)
self.admin_action.setCheckable(False)
self.formLayout_2.setWidget(1, QFormLayout.ItemRole.FieldRole, self.admin_action)
self.tabWidget.addTab(self.admin, "")
self.gridLayout.addWidget(self.tabWidget, 0, 0, 1, 1)
@@ -963,7 +976,7 @@ class Ui_MainWindow(object):
" hinzuf\u00fcgen", None))
#if QT_CONFIG(tooltip)
self.btn_extract_data_from_document.setToolTip(QCoreApplication.translate("MainWindow", u"Die Apparatsdetails werden aus dem Dokument gelesen und eingetragen\n"
"Einige Angaben m\u00fcssen ggf angepasst werden", None))
"Die gew\u00fcnschten Medien werden automatisch in die Medienliste eingetragen, evtl. unvollst\u00e4ndig, da eBooks nicht erfasst werden k\u00f6nnenEinige Angaben m\u00fcssen ggf angepasst werden", None))
#endif // QT_CONFIG(tooltip)
self.btn_extract_data_from_document.setText(QCoreApplication.translate("MainWindow", u"Daten aus Dokument\n"
"\u00fcbernehmen", None))

View File

@@ -373,7 +373,7 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat):
self.setWidget(UpdateSignatures())
self.admin_action.setTitle("Medien bearbeiten")
else:
self.hideWidget()
# self.hideWidget()
self.admin_action.setTitle("")
def toggleButton(self, button: QtWidgets.QCheckBox):
@@ -1224,12 +1224,14 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat):
signatures = csv_to_list(file)
# add the data to the database
return signatures
if file_type == "docx":
if file_type in ("docx", "doc"):
data = word_to_semap(file)
log.info("Converted data from semap file")
log.debug("Got the data: {}", data)
return data
else:
raise ValueError("Dateityp wird nicht unterstützt")
def import_data_from_document(self):
global valid_input
@@ -1241,6 +1243,7 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat):
self.prof_mail.setText(data.mail)
self.prof_tel_nr.setText(str(data.phoneNumber).replace("-", ""))
self.app_name.setText(data.title)
if len(data.title_suggestions) > 0:
# create a dialog that has a dropdown with the suggestions, and oc and cancel button. on ok return the selected text and set it as title
dialog = QtWidgets.QDialog()
@@ -1271,6 +1274,7 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat):
self.app_name.setText(dropdown.currentText().split(" [")[0].strip())
else:
self.app_name.setText("CHANGEME")
# self.app_name.setText(data.title)
subjects = self.db.getSubjects()
subjects = [subject[1] for subject in subjects]
@@ -1287,8 +1291,10 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat):
if data.eternal:
self.check_eternal_app.setChecked(True)
self.validate_semester()
if data.books != []:
self.btn_check_file_threaded(data)
def btn_check_file_threaded(self):
def btn_check_file_threaded(self, c_document: Optional[SemapDocument] = None):
for runner in self.bookGrabber:
if not runner.isRunning():
runner.deleteLater()
@@ -1335,7 +1341,10 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat):
prof_id = self.db.getProfId(self.profdata)
# log.debug("Prof ID is None", prof_id)
document = self.extract_document_data()
document = None
if c_document is None or not isinstance(c_document, SemapDocument):
document = self.extract_document_data()
if document is None:
log.error("Document is None")
elif isinstance(document, SemapDocument):
@@ -1410,7 +1419,7 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat):
)
prof.title = self.prof_title.text()
apparat = Apparat(
appnr=self.active_apparat,
appnr=int(self.drpdwn_app_nr.currentText()),
name=self.app_name.text(),
created_semester=self.generateSemester(),
eternal=1 if self.check_eternal_app.isChecked() else 0,
@@ -1433,7 +1442,8 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat):
return
appdata = self.db.getAllAparats()
# merge self.appdata and appdata, remove duplicates
self.apparats = list(set(self.apparats + appdata))
self.apparats = self.__uniques(self.apparats, appdata)
self.apparats = natsorted(self.apparats, key=lambda x: x[4], reverse=True)
self.update_apparat_list()
@@ -1452,6 +1462,16 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat):
self.__clear_fields()
return True
def __uniques(self, list1, list2):
seen = set()
unique_list = []
for item in list1 + list2:
identifier = (item.appnr, item.name)
if identifier not in seen:
seen.add(identifier)
unique_list.append(item)
return unique_list
def send_mail_preview(self):
pass

View File

@@ -21,7 +21,7 @@ class FilePicker:
files, _ = filepicker.getOpenFileNames(
caption="Open file",
dir=self.last_path,
filter="Unterstützte Dateien (*.docx *.csv *.eml );;Word (*.docx);;CSV Files (*.csv);;Mail (*.eml)",
filter="Unterstützte Dateien (*.docx *.doc *.csv *.eml );;Word (*.docx *.doc);;CSV Files (*.csv);;Mail (*.eml)",
)
if files:
self.last_path = files[0]

View File

@@ -1,4 +1,4 @@
def create_blob(file: str):
def create_blob(file: str) -> bytes:
"""
Creates a blob from a file.
"""

View File

@@ -1,9 +1,10 @@
import os
from pyramid.config import Configurator
from wsgiref.simple_server import WSGIRequestHandler
from src import LOG_DIR
import logging
import os
from wsgiref.simple_server import WSGIRequestHandler
from pyramid.config import Configurator
from src import LOG_DIR
log_path = os.path.join(LOG_DIR, "web_documentation.log")
@@ -31,7 +32,7 @@ class QuietHandler(WSGIRequestHandler):
pass
def website():
def website() -> object:
config = Configurator()
# Set up static file serving from the 'site/' directory
@@ -40,4 +41,4 @@ def website():
)
app = config.make_wsgi_app()
return app
return app # type: ignore

View File

@@ -2,9 +2,9 @@ import pickle
from typing import Any
def load_pickle(data: Any):
def load_pickle(data: Any) -> Any:
return pickle.loads(data)
def dump_pickle(data: Any):
def dump_pickle(data: Any) -> bytes:
return pickle.dumps(data)

View File

@@ -16,7 +16,7 @@ logger = log
font = "Cascadia Mono"
def print_document(file: str):
def print_document(file: str) -> None:
# send document to printer as attachment of email
import smtplib
from email.mime.application import MIMEApplication
@@ -98,7 +98,7 @@ class SemesterDocument:
self.filename = filename
if full:
log.info("Full document generation")
self.cleanup()
self.cleanup
log.info("Cleanup done")
self.make_document()
log.info("Document created")
@@ -221,15 +221,15 @@ class SemesterDocument:
self.create_sorted_table()
def save_document(self, name):
def save_document(self, name: str) -> None:
# Save the document
self.doc.save(name)
def create_pdf(self):
def create_pdf(self) -> None:
# Save the document
import comtypes.client
word = comtypes.client.CreateObject("Word.Application")
word = comtypes.client.CreateObject("Word.Application") # type: ignore
self.save_document(self.filename + ".docx")
docpath = os.path.abspath(self.filename + ".docx")
doc = word.Documents.Open(docpath)
@@ -240,13 +240,13 @@ class SemesterDocument:
log.debug("PDF saved")
@property
def cleanup(self):
def cleanup(self) -> None:
if os.path.exists(f"{self.filename}.docx"):
os.remove(f"{self.filename}.docx")
os.remove(f"{self.filename}.pdf")
@property
def send(self):
def send(self) -> None:
print_document(self.filename + ".pdf")
log.debug("Document sent to printer")
@@ -309,11 +309,11 @@ class SemapSchilder:
self.doc.save(f"{self.filename}.docx")
log.debug(f"Document saved as {self.filename}.docx")
def create_pdf(self):
def create_pdf(self) -> None:
# Save the document
import comtypes.client
word = comtypes.client.CreateObject("Word.Application")
word = comtypes.client.CreateObject("Word.Application") # type: ignore
self.save_document()
docpath = os.path.abspath(f"{self.filename}.docx")
doc = word.Documents.Open(docpath)
@@ -323,14 +323,14 @@ class SemapSchilder:
word.Quit()
log.debug("PDF saved")
def cleanup(self):
def cleanup(self) -> None:
if os.path.exists(f"{self.filename}.docx"):
os.remove(f"{self.filename}.docx")
if os.path.exists(f"{self.filename}.pdf"):
os.remove(f"{self.filename}.pdf")
@property
def send(self):
def send(self) -> None:
print_document(self.filename + ".pdf")
log.debug("Document sent to printer")