1 Commits

Author SHA1 Message Date
7b43826ef5 fix: update eml support 2026-02-12 10:15:48 +01:00
27 changed files with 288 additions and 473 deletions

View File

@@ -373,31 +373,26 @@ CREATE TABLE IF NOT EXISTS user_preferences (
2. Convert: `pyside6-uic dialog.ui -o dialog_ui.py` 2. Convert: `pyside6-uic dialog.ui -o dialog_ui.py`
3. Create dialog class in `src/ui/dialogs/` 3. Create dialog class in `src/ui/dialogs/`
4. Connect signals to business logic 4. Connect signals to business logic
### Building Documentation
## 📚 Documentation ```bash
# Using uv
uv run mkdocs build
uv run mkdocs serve # View at http://localhost:8000
- **[User Manual](docs/)**: Complete user guide built with Zola and the Tanuki theme # Or with activated venv
- View documentation at `http://localhost:8000` when running the application mkdocs build
mkdocs serve
```*[API Documentation](docs/)**: Detailed module documentation
- **[User Manual](docs/index.md)**: Complete user guide (MkDocs)
### Building Documentation ### Building Documentation
The documentation is built using [Zola](https://www.getzola.org/) with the Tanuki theme.
```bash ```bash
# Build documentation using the provided script mkdocs build
.\build_docs.ps1 mkdocs serve # View at http://localhost:8000
# Or manually:
cd docs
zola build
# Serve documentation locally for development
cd docs
zola serve # View at http://127.0.0.1:1111
``` ```
The built documentation is served automatically when you run the application and access the documentation menu.
## 🤝 Contributing ## 🤝 Contributing
Contributions are welcome! Please follow these guidelines: Contributions are welcome! Please follow these guidelines:

View File

@@ -1,6 +1,6 @@
from typing import Optional, Any, Union from typing import Optional, Any, Union
from dataclasses import dataclass from dataclasses import dataclass
from omegaconf import OmegaConf, DictConfig, ListConfig from omegaconf import OmegaConf, DictConfig
import os import os
from pathlib import Path from pathlib import Path
@@ -140,7 +140,7 @@ class Config:
""" """
_config: Optional[Union[DictConfig, ListConfig]] = None _config: Optional[DictConfig] = None
config_exists: bool = True config_exists: bool = True
def __init__(self, config_path: str): def __init__(self, config_path: str):
@@ -183,25 +183,22 @@ class Config:
""" """
Reloads the configuration from the file. Reloads the configuration from the file.
""" """
if self.config_path is not None: self._config = OmegaConf.load(self.config_path)
self._config = OmegaConf.load(self.config_path)
@property @property
def zotero(self): def zotero(self):
if self._config is None:
raise RuntimeError("Configuration not loaded")
return Zotero(**self._config.zotero) return Zotero(**self._config.zotero)
def get_zotero_attr(self, name: str): @property
def zotero_attr(self, name: str):
return getattr(self.zotero, name) return getattr(self.zotero, name)
def set_zotero_attr(self, name: str, value: Any): @zotero_attr.setter
def zotero_attr(self, name: str, value: Any):
self.zotero._setattr(name, value) self.zotero._setattr(name, value)
@property @property
def database(self): def database(self):
if self._config is None:
raise RuntimeError("Configuration not loaded")
return Database(**self._config.database) return Database(**self._config.database)
@property @property
@@ -214,57 +211,43 @@ class Config:
@property @property
def openAI(self): def openAI(self):
if self._config is None:
raise RuntimeError("Configuration not loaded")
return OpenAI(**self._config.openAI) return OpenAI(**self._config.openAI)
@property @property
def mail(self): def mail(self):
if self._config is None:
raise RuntimeError("Configuration not loaded")
return Mail(**self._config.mail) return Mail(**self._config.mail)
def mail_attr(self, name: str): def mail_attr(self, name: str):
return getattr(self.mail, name) return getattr(self.mail, name)
def set_mail_attr(self, name: str, value: Any): def set_mail_attr(self, name: str, value: Any):
if self._config is not None: OmegaConf.update(self._config, f"mail.{name}", value)
OmegaConf.update(self._config, f"mail.{name}", value)
def set_database_attr(self, name: str, value: Any): def set_database_attr(self, name: str, value: Any):
if self._config is not None: OmegaConf.update(self._config, f"database.{name}", value)
OmegaConf.update(self._config, f"database.{name}", value)
def set_zotero_attr(self, name: str, value: Any): def set_zotero_attr(self, name: str, value: Any):
if self._config is not None: OmegaConf.update(self._config, f"zotero.{name}", value)
OmegaConf.update(self._config, f"zotero.{name}", value)
def set_openai_attr(self, name: str, value: Any): def set_openai_attr(self, name: str, value: Any):
if self._config is not None: OmegaConf.update(self._config, f"openAI.{name}", value)
OmegaConf.update(self._config, f"openAI.{name}", value)
def set_icon_attr(self, name: str, value: Any): def set_icon_attr(self, name: str, value: Any):
if self._config is not None: OmegaConf.update(self._config, f"icons.{name}", value)
OmegaConf.update(self._config, f"icons.{name}", value)
@property @property
def save_path(self): def save_path(self):
if self._config is None:
raise RuntimeError("Configuration not loaded")
return self._config.save_path return self._config.save_path
@save_path.setter @save_path.setter
def save_path(self, value: str): def save_path(self, value: str):
if self._config is not None: self._config.save_path = value
self._config.save_path = value
def load_config(self, path, filename): def load_config(self, path, filename):
return OmegaConf.load(os.path.join(path, filename)) return OmegaConf.load(os.path.join(path, filename))
@property @property
def icons(self): def icons(self):
if self._config is None:
raise RuntimeError("Configuration not loaded")
icons = Icons() icons = Icons()
icons.assign("path", self._config.icon_path) icons.assign("path", self._config.icon_path)
icons.assign("colors", self._config.colors) icons.assign("colors", self._config.colors)

1
docs/themes/tanuki vendored Submodule

Submodule docs/themes/tanuki added at f81db54c4e

View File

@@ -41,14 +41,34 @@ dev = [
"pytest", "pytest",
"pytest-cov", "pytest-cov",
"pyinstaller>=6.17.0", "pyinstaller>=6.17.0",
"ty>=0.0.15",
] ]
swbtest = ["alive-progress>=3.3.0"] swbtest = ["alive-progress>=3.3.0"]
[tool.ruff] [tool.bumpversion]
line-length = 88 current_version = "1.0.2"
target-version = "py313" parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
serialize = ["{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 = true
commit = true
message = "Bump version: {current_version} → {new_version}"
moveable_tags = []
commit_args = ""
setup_hooks = []
pre_commit_hooks = []
post_commit_hooks = []
[[tool.bumpversion.files]]
filename = "src/__init__.py"
[[tool.bumpversion.files]]
filename = ".version"
[[tool.uv.index]] [[tool.uv.index]]
name = "gitea" name = "gitea"

View File

@@ -33,8 +33,6 @@ if not _user_log_dir:
if not _user_config_dir: if not _user_config_dir:
_user_config_dir = str(get_app_base_path() / "config") _user_config_dir = str(get_app_base_path() / "config")
from config import Config # noqa: E402
LOG_DIR: str = _user_log_dir LOG_DIR: str = _user_log_dir
CONFIG_DIR: str = _user_config_dir CONFIG_DIR: str = _user_config_dir
@@ -51,6 +49,8 @@ except Exception:
Path(LOG_DIR).mkdir(parents=True, exist_ok=True) Path(LOG_DIR).mkdir(parents=True, exist_ok=True)
Path(CONFIG_DIR).mkdir(parents=True, exist_ok=True) Path(CONFIG_DIR).mkdir(parents=True, exist_ok=True)
from config import Config
settings = Config(f"{CONFIG_DIR}/config.yaml") settings = Config(f"{CONFIG_DIR}/config.yaml")
DATABASE_DIR: Union[Path, str] = ( # type: ignore DATABASE_DIR: Union[Path, str] = ( # type: ignore

View File

@@ -1216,9 +1216,9 @@ class Database:
Optional[int]: the id of the apparat Optional[int]: the id of the apparat
""" """
log.debug("Creating apparat: {} - {}", app.appnr, app.name)
app = apparat.apparat app = apparat.apparat
prof = apparat.prof prof = apparat.prof
log.debug("Creating apparat: {} - {}", app.appnr, app.name)
present_prof = self.getProfByName(prof.name()) present_prof = self.getProfByName(prof.name())
prof_id = present_prof.id prof_id = present_prof.id
log.debug("Present prof: {}", preview(present_prof, 300)) log.debug("Present prof: {}", preview(present_prof, 300))

View File

@@ -24,5 +24,5 @@ class DocumentationThread(QThread):
self._process.terminate() # terminate the subprocess self._process.terminate() # terminate the subprocess
try: try:
self._process.wait(timeout=5) # wait up to 5 seconds self._process.wait(timeout=5) # wait up to 5 seconds
except Exception: except:
self._process.kill() # force kill if it doesn't stop self._process.kill() # force kill if it doesn't stop

View File

@@ -12,7 +12,7 @@ from .models import (
Subjects, Subjects,
XMLMailSubmission, XMLMailSubmission,
) )
from .constants import * # noqa: F403 from .constants import *
from .semester import Semester from .semester import Semester
__all__ = [ __all__ = [

View File

@@ -21,7 +21,7 @@ class Prof:
telnr: str | None = None telnr: str | None = None
# add function that sets the data based on a dict # add function that sets the data based on a dict
def from_dict(self, data: dict[str, Union[str, int]]) -> 'Prof': def from_dict(self, data: dict[str, Union[str, int]]):
for key, value in data.items(): for key, value in data.items():
if hasattr(self, key): if hasattr(self, key):
setattr(self, key, value) setattr(self, key, value)
@@ -38,40 +38,27 @@ class Prof:
self._title = 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[Union[int, str, None], ...]) -> 'Prof': def from_tuple(self, data: tuple[Union[str, int], ...]) -> Prof:
self.id = data[0] if data[0] is not None and isinstance(data[0], int) else None self.id = data[0]
self._title = str(data[1]) if data[1] is not None else None self._title = data[1]
self.firstname = str(data[2]) if data[2] is not None else None self.firstname = data[2]
self.lastname = str(data[3]) if data[3] is not None else None self.lastname = data[3]
self.fullname = str(data[4]) if data[4] is not None else None self.fullname = data[4]
self.mail = str(data[5]) if data[5] is not None else None self.mail = data[5]
self.telnr = str(data[6]) if data[6] is not None else None self.telnr = data[6]
return self return self
def name(self, comma: bool = False) -> Optional[str]: def name(self, comma: bool = False) -> Optional[str]:
if self.firstname is None and self.lastname is None: if self.firstname is None and self.lastname is None:
if self.fullname and "," in self.fullname: if "," in self.fullname:
parts = self.fullname.split(",") self.firstname = self.fullname.split(",")[1].strip()
if len(parts) >= 2: self.lastname = self.fullname.split(",")[0].strip()
self.firstname = parts[1].strip()
self.lastname = parts[0].strip()
else: else:
return self.fullname return self.fullname
if comma: if comma:
if self.lastname and self.firstname: return f"{self.lastname}, {self.firstname}"
return f"{self.lastname}, {self.firstname}" return f"{self.lastname} {self.firstname}"
elif self.lastname:
return self.lastname
elif self.firstname:
return f", {self.firstname}"
elif self.lastname and self.firstname:
return f"{self.lastname} {self.firstname}"
elif self.lastname:
return self.lastname
elif self.firstname:
return self.firstname
return self.fullname
@dataclass @dataclass
@@ -103,12 +90,10 @@ class BookData:
if isinstance(self.language, list) and self.language: if isinstance(self.language, list) and self.language:
self.language = [lang.strip() for lang in self.language if lang.strip()] self.language = [lang.strip() for lang in self.language if lang.strip()]
self.language = ",".join(self.language) self.language = ",".join(self.language)
if self.year is not None: self.year = regex.sub(r"[^\d]", "", str(self.year)) if self.year else None
year_str = regex.sub(r"[^\d]", "", str(self.year))
self.year = int(year_str) if year_str else None
self.in_library = True if self.signature else False self.in_library = True if self.signature else False
def from_dict(self, data: dict[str, Any]) -> 'BookData': def from_dict(self, data: dict) -> BookData:
for key, value in data.items(): for key, value in data.items():
setattr(self, key, value) setattr(self, key, value)
return self return self
@@ -147,26 +132,23 @@ class BookData:
del data_dict["old_book"] del data_dict["old_book"]
return json.dumps(data_dict, ensure_ascii=False) return json.dumps(data_dict, ensure_ascii=False)
def from_dataclass(self, data_obj: Optional[Any]) -> None: def from_dataclass(self, dataclass: Optional[Any]) -> None:
if data_obj is None: if dataclass is None:
return return
for key, value in data_obj.__dict__.items(): for key, value in dataclass.__dict__.items():
setattr(self, key, value) setattr(self, key, value)
def get_book_type(self) -> str: def get_book_type(self) -> str:
if self.pages and "Online" in self.pages: if "Online" in self.pages:
return "eBook" return "eBook"
return "Druckausgabe" return "Druckausgabe"
def from_string(self, data: str) -> 'BookData': def from_string(self, data: str) -> BookData:
ndata = json.loads(data) ndata = json.loads(data)
# Create a new BookData instance and set its attributes
book_data = BookData()
for key, value in ndata.items():
setattr(book_data, key, value)
return book_data
def from_LehmannsSearchResult(self, result: Any) -> 'BookData': return BookData(**ndata)
def from_LehmannsSearchResult(self, result: Any) -> BookData:
self.title = result.title self.title = result.title
self.author = "; ".join(result.authors) if result.authors else None self.author = "; ".join(result.authors) if result.authors else None
self.edition = str(result.edition) if result.edition else None self.edition = str(result.edition) if result.edition else None
@@ -188,11 +170,23 @@ class BookData:
def edition_number(self) -> Optional[int]: def edition_number(self) -> Optional[int]:
if self.edition is None: if self.edition is None:
return 0 return 0
match = regex.search(r"(\d+)", self.edition or "") match = regex.search(r"(\d+)", self.edition)
if match: if match:
return int(match.group(1)) return int(match.group(1))
return 0 return 0
def to_book(self) -> Book:
return Book(
author=self.author,
year=self.year,
edition=self.edition,
title=self.title,
location=self.place,
publisher=self.publisher,
signature=self.signature,
internal_notes=None,
)
@dataclass @dataclass
class MailData: class MailData:
@@ -233,13 +227,13 @@ class Subjects(Enum):
return self.value[0] return self.value[0]
@property @property
def subject_name(self) -> str: def name(self) -> str:
return self.value[1] return self.value[1]
@classmethod @classmethod
def get_index(cls, name: str) -> Optional[int]: def get_index(cls, name: str) -> Optional[int]:
for i in cls: for i in cls:
if i.subject_name == name: if i.name == name:
return i.id - 1 return i.id - 1
return None return None
@@ -308,15 +302,18 @@ class ApparatData:
@dataclass @dataclass
class XMLMailSubmission: class XMLMailSubmission:
name: str | None name: str | None = None
lastname: str | None lastname: str | None = None
title: str | None title: str | None = None
telno: int | None telno: int | None = None
email: str | None email: str | None = None
app_name: str | None app_name: str | None = None
subject: str | None subject: str | None = None
semester: Semester | None semester: Semester | None = None
books: list[BookData] | None books: list[BookData] | None = None
dauerapparat: bool = False
# def __post_init__(self) -> None:
# convert semester to string
@dataclass @dataclass
@@ -373,24 +370,26 @@ class Book:
@dataclass @dataclass
class SemapDocument: class SemapDocument:
subject: str | None = None subject: str | None
phoneNumber: int | None = None phoneNumber: int | None
mail: str | None = None mail: str | None
title: str | None = None title: str | None
personName: str | None = None personName: str | None
personTitle: str | None = None personTitle: str | None
title_suggestions: list[str] = field(default_factory=list) title_suggestions: list[str] = None
semester: Union[str, 'Semester', None] = None semester: Union[str, Semester] = None
books: list[Book] = field(default_factory=list) books: list[Book] = None
eternal: bool = False eternal: bool = False
title_length: int = 0 title_length: int = 0
title_max_length: int = 0 title_max_length: int = 0
def __post_init__(self) -> None: def __post_init__(self) -> None:
""".""" """."""
if self.phoneNumber is not None: self.title_suggestions = []
phone_str = regex.sub(r"[^\d]", "", str(self.phoneNumber)) self.phoneNumber = int(
self.phoneNumber = int(phone_str) if phone_str else None regex.sub(r"[^\d]", "", str(self.phoneNumber)),
)
self.title_length = len(self.title) + 3 + len(self.personName.split(",")[0])
@property @property
def nameSetter(self): def nameSetter(self):
@@ -416,7 +415,7 @@ class SemapDocument:
def renameSemester(self) -> None: def renameSemester(self) -> None:
from src.services.openai import semester_converter from src.services.openai import semester_converter
if self.semester and isinstance(self.semester, str): if self.semester:
if ", Dauer" in self.semester: if ", Dauer" in self.semester:
self.semester = self.semester.split(",")[0] self.semester = self.semester.split(",")[0]
self.eternal = True self.eternal = True

View File

@@ -1,4 +1,4 @@
"""Semester helper class. """Semester helper class
A small utility around the *German* academic calendar that distinguishes A small utility around the *German* academic calendar that distinguishes
between *Wintersemester* (WiSe) and *Sommersemester* (SoSe). between *Wintersemester* (WiSe) and *Sommersemester* (SoSe).
@@ -7,7 +7,7 @@ Key points
---------- ----------
* A **`Semester`** is identified by a *term* ("SoSe" or "WiSe") and the last two * A **`Semester`** is identified by a *term* ("SoSe" or "WiSe") and the last two
digits of the calendar year in which the term *starts*. digits of the calendar year in which the term *starts*.
* Formatting **never** pads the year with a leading zero - so ``6`` stays ``6``. * Formatting **never** pads the year with a leading zero so ``6`` stays ``6``.
* ``offset(n)`` and the static ``generate_missing`` reliably walk the timeline * ``offset(n)`` and the static ``generate_missing`` reliably walk the timeline
one semester at a time with correct year transitions: one semester at a time with correct year transitions:
@@ -26,13 +26,13 @@ class Semester:
"""Represents a German university semester (WiSe or SoSe).""" """Represents a German university semester (WiSe or SoSe)."""
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Class-level defaults - will be *copied* to each instance and then # Classlevel defaults will be *copied* to each instance and then
# potentially overwritten in ``__init__``. # potentially overwritten in ``__init__``.
# ------------------------------------------------------------------ # ------------------------------------------------------------------
_year: int | None = None # Will be set in __post_init__ _year: int | None = None # Will be set in __post_init__
_semester: str | None = None # "WiSe" or "SoSe" - set later _semester: str | None = None # "WiSe" or "SoSe" set later
_month: int | None = None # Will be set in __post_init__ _month: int | None = None # Will be set in __post_init__
value: str | None = None # Human-readable label, e.g. "WiSe 23/24" value: str | None = None # Humanreadable label, e.g. "WiSe 23/24"
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Construction helpers # Construction helpers
@@ -89,22 +89,20 @@ class Semester:
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def _generate_semester_from_month(self) -> None: def _generate_semester_from_month(self) -> None:
"""Infer *WiSe* / *SoSe* from the month attribute.""" """Infer *WiSe* / *SoSe* from the month attribute."""
if self._month is not None: self._semester = "WiSe" if (self._month <= 3 or self._month > 9) else "SoSe"
self._semester = "WiSe" if (self._month <= 3 or self._month > 9) else "SoSe"
else:
self._semester = "WiSe" # Default value if month is None
def _compute_value(self) -> None: def _compute_value(self) -> None:
"""Human-readable semester label - e.g. ``WiSe 23/24`` or ``SoSe 24``.""" """Humanreadable semester label e.g. ``WiSe 23/24`` or ``SoSe 24``."""
if self._year is not None: year = self._year
year = self._year if self._semester == "WiSe":
if self._semester == "WiSe": next_year = (year + 1) % 100 # wrap 99 → 0
next_year = (year + 1) % 100 # wrap 99 → 0
self.value = f"WiSe {year}/{next_year}" self.value = f"WiSe {year}/{next_year}"
else: # SoSe else: # SoSe
self.value = f"SoSe {year}" # year may only be the last two digits, so we don't want to pad it with a leading zero
else: if len(str(year)) > 2:
self.value = "<invalid Semester>" year = int(str(year)[-2:])
self.value = f"SoSe {year}"
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Public API # Public API
@@ -122,12 +120,10 @@ class Semester:
if value == 0: if value == 0:
return Semester(self._year, self._semester) return Semester(self._year, self._semester)
if self._year is None:
raise ValueError("Cannot offset from a semester with no year")
current_idx = self._year * 2 + (0 if self._semester == "SoSe" else 1) current_idx = self._year * 2 + (0 if self._semester == "SoSe" else 1)
target_idx = current_idx + value target_idx = current_idx + value
if target_idx < 0: if target_idx < 0:
raise ValueError("offset would result in a negative year - not supported") raise ValueError("offset would result in a negative year not supported")
new_year, semester_bit = divmod(target_idx, 2) new_year, semester_bit = divmod(target_idx, 2)
new_semester = "SoSe" if semester_bit == 0 else "WiSe" new_semester = "SoSe" if semester_bit == 0 else "WiSe"
@@ -171,14 +167,10 @@ class Semester:
@property @property
def year(self) -> int: def year(self) -> int:
if self._year is None:
raise ValueError("Year is not set for this semester")
return self._year return self._year
@property @property
def semester(self) -> str: def semester(self) -> str:
if self._semester is None:
raise ValueError("Semester is not set for this semester")
return self._semester return self._semester
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -189,14 +181,14 @@ class Semester:
"""Return all consecutive semesters from *start* to *end* (inclusive).""" """Return all consecutive semesters from *start* to *end* (inclusive)."""
if not isinstance(start, Semester) or not isinstance(end, Semester): if not isinstance(start, Semester) or not isinstance(end, Semester):
raise TypeError("start and end must be Semester instances") raise TypeError("start and end must be Semester instances")
if start.is_future_semester(end) and not start.is_match(end): if start.is_future_semester(end) and not start.isMatch(end):
raise ValueError("'start' must not be after 'end'") raise ValueError("'start' must not be after 'end'")
chain: list[str] = [str(start)] chain: list[Semester] = [start.value]
current = start current = start
while not current.is_match(end): while not current.isMatch(end):
current = current.next current = current.next
chain.append(str(current)) chain.append(current.value)
if len(chain) > 1000: # sanity guard if len(chain) > 1000: # sanity guard
raise RuntimeError("generate_missing exceeded sane iteration limit") raise RuntimeError("generate_missing exceeded sane iteration limit")
return chain return chain
@@ -206,9 +198,9 @@ class Semester:
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@classmethod @classmethod
def from_string(cls, s: str) -> Semester: def from_string(cls, s: str) -> Semester:
"""Parse a human-readable semester label and return a :class:`Semester`. """Parse a humanreadable semester label and return a :class:`Semester`.
Accepted formats (case-insensitive):: Accepted formats (caseinsensitive)::
"SoSe <YY>" → SoSe of year YY "SoSe <YY>" → SoSe of year YY
"WiSe <YY>/<YY+1>" → Winter term starting in YY "WiSe <YY>/<YY+1>" → Winter term starting in YY
@@ -223,7 +215,7 @@ class Semester:
m = re.fullmatch(pattern, s, flags=re.IGNORECASE) m = re.fullmatch(pattern, s, flags=re.IGNORECASE)
if not m: if not m:
raise ValueError( raise ValueError(
"invalid semester string format - expected 'SoSe YY' or 'WiSe YY/YY' (spacing flexible)", "invalid semester string format expected 'SoSe YY' or 'WiSe YY/YY' (spacing flexible)",
) )
term_raw, y1_str, y2_str = m.groups() term_raw, y1_str, y2_str = m.groups()
@@ -247,7 +239,7 @@ class Semester:
return cls(year, "WiSe") return cls(year, "WiSe")
# ------------------------- quick self-test ------------------------- # ------------------------- quick selftest -------------------------
if __name__ == "__main__": if __name__ == "__main__":
# Chain generation demo ------------------------------------------------ # Chain generation demo ------------------------------------------------
s_start = Semester(6, "SoSe") # SoSe 6 s_start = Semester(6, "SoSe") # SoSe 6

View File

@@ -123,7 +123,7 @@ class Database:
try: try:
if self.db_path is not None: if self.db_path is not None:
self.run_migrations() self.run_migrations()
except (sql.Error, OSError, IOError) as e: except Exception as e:
log.error(f"Error while running migrations: {e}") log.error(f"Error while running migrations: {e}")
# --- Migration helpers integrated into Database --- # --- Migration helpers integrated into Database ---
@@ -212,9 +212,9 @@ class Database:
).__str__() ).__str__()
return result[0] return result[0]
def getElsaMediaType(self, media_id): def getElsaMediaType(self, id):
query = "SELECT type FROM elsa_media WHERE id=?" query = "SELECT type FROM elsa_media WHERE id=?"
return self.query_db(query, (media_id,), one=True)[0] return self.query_db(query, (id,), one=True)[0]
def get_db_contents(self) -> Union[List[Tuple[Any]], None]: def get_db_contents(self) -> Union[List[Tuple[Any]], None]:
""" """
@@ -736,7 +736,7 @@ class Database:
try: try:
bloat.debug("Recreated file blob size: {} bytes", len(blob)) bloat.debug("Recreated file blob size: {} bytes", len(blob))
bloat.debug("Recreated file blob (preview): {}", preview(blob, 2000)) bloat.debug("Recreated file blob (preview): {}", preview(blob, 2000))
except (TypeError, UnicodeDecodeError, ValueError): except Exception:
bloat.debug("Recreated file blob (preview): {}", preview(blob, 2000)) bloat.debug("Recreated file blob (preview): {}", preview(blob, 2000))
tempdir = settings.database.temp.expanduser() tempdir = settings.database.temp.expanduser()
if not tempdir.exists(): if not tempdir.exists():
@@ -990,16 +990,16 @@ class Database:
person = Prof() person = Prof()
return person.from_tuple(data) return person.from_tuple(data)
def getProf(self, prof_id) -> Prof: def getProf(self, id) -> Prof:
"""Get a professor based on the id """Get a professor based on the id
Args: Args:
prof_id ([type]): the id of the professor id ([type]): the id of the professor
Returns: Returns:
Prof: a Prof object containing the data of the professor Prof: a Prof object containing the data of the professor
""" """
data = self.query_db("SELECT * FROM prof WHERE id=?", (prof_id,), one=True) data = self.query_db("SELECT * FROM prof WHERE id=?", (id,), one=True)
return Prof().from_tuple(data) return Prof().from_tuple(data)
def getProfs(self) -> list[Prof]: def getProfs(self) -> list[Prof]:
@@ -1278,17 +1278,17 @@ class Database:
# print(apparat_nr, app_id) # print(apparat_nr, app_id)
self.query_db("UPDATE media SET deleted=1 WHERE app_id=?", (app_id,)) self.query_db("UPDATE media SET deleted=1 WHERE app_id=?", (app_id,))
def isEternal(self, apparat_id): def isEternal(self, id):
"""check if the apparat is eternal (dauerapparat) """check if the apparat is eternal (dauerapparat)
Args: Args:
apparat_id (int): the id of the apparat to be checked id (int): the id of the apparat to be checked
Returns: Returns:
int: the state of the apparat int: the state of the apparat
""" """
return self.query_db( return self.query_db(
"SELECT dauer FROM semesterapparat WHERE appnr=?", (apparat_id,), one=True "SELECT dauer FROM semesterapparat WHERE appnr=?", (id,), one=True
) )
def getApparatName(self, app_id: Union[str, int], prof_id: Union[str, int]): def getApparatName(self, app_id: Union[str, int], prof_id: Union[str, int]):

View File

@@ -1,7 +1,6 @@
import os import os
from datetime import datetime from datetime import datetime
from os.path import basename from os.path import basename
from pathlib import Path
from docx import Document from docx import Document
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
@@ -70,20 +69,20 @@ class SemesterDocument:
full: bool = False, full: bool = False,
): ):
assert isinstance(apparats, list), SemesterError( assert isinstance(apparats, list), SemesterError(
"Apparats must be a list of tuples", "Apparats must be a list of tuples"
) )
assert all(isinstance(apparat, tuple) for apparat in apparats), SemesterError( assert all(isinstance(apparat, tuple) for apparat in apparats), SemesterError(
"Apparats must be a list of tuples", "Apparats must be a list of tuples"
) )
assert all(isinstance(apparat[0], int) for apparat in apparats), SemesterError( assert all(isinstance(apparat[0], int) for apparat in apparats), SemesterError(
"Apparat numbers must be integers", "Apparat numbers must be integers"
) )
assert all(isinstance(apparat[1], str) for apparat in apparats), SemesterError( assert all(isinstance(apparat[1], str) for apparat in apparats), SemesterError(
"Apparat names must be strings", "Apparat names must be strings"
) )
assert isinstance(semester, str), SemesterError("Semester must be a string") assert isinstance(semester, str), SemesterError("Semester must be a string")
assert "." not in filename and isinstance(filename, str), SemesterError( assert "." not in filename and isinstance(filename, str), SemesterError(
"Filename must be a string and not contain an extension", "Filename must be a string and not contain an extension"
) )
self.doc = Document() self.doc = Document()
self.apparats = apparats self.apparats = apparats
@@ -109,7 +108,8 @@ class SemesterDocument:
log.info("Document printed") log.info("Document printed")
def set_table_border(self, table): def set_table_border(self, table):
"""Adds a full border to the table. """
Adds a full border to the table.
:param table: Table object to which the border will be applied. :param table: Table object to which the border will be applied.
""" """
@@ -150,8 +150,7 @@ class SemesterDocument:
trPr = row._tr.get_or_add_trPr() # Get or add the <w:trPr> element trPr = row._tr.get_or_add_trPr() # Get or add the <w:trPr> element
trHeight = OxmlElement("w:trHeight") trHeight = OxmlElement("w:trHeight")
trHeight.set( trHeight.set(
qn("w:val"), qn("w:val"), str(int(Pt(15).pt * 20))
str(int(Pt(15).pt * 20)),
) # Convert points to twips ) # Convert points to twips
trHeight.set(qn("w:hRule"), "exact") # Use "exact" for fixed height trHeight.set(qn("w:hRule"), "exact") # Use "exact" for fixed height
trPr.append(trHeight) trPr.append(trHeight)
@@ -234,7 +233,7 @@ class SemesterDocument:
self.save_document(self.filename + ".docx") self.save_document(self.filename + ".docx")
docpath = os.path.abspath(self.filename + ".docx") docpath = os.path.abspath(self.filename + ".docx")
doc = word.Documents.Open(docpath) doc = word.Documents.Open(docpath)
curdir = Path.cwd() curdir = os.getcwd()
doc.SaveAs(f"{curdir}/{self.filename}.pdf", FileFormat=17) doc.SaveAs(f"{curdir}/{self.filename}.pdf", FileFormat=17)
doc.Close() doc.Close()
word.Quit() word.Quit()
@@ -318,7 +317,7 @@ class SemapSchilder:
self.save_document() self.save_document()
docpath = os.path.abspath(f"{self.filename}.docx") docpath = os.path.abspath(f"{self.filename}.docx")
doc = word.Documents.Open(docpath) doc = word.Documents.Open(docpath)
curdir = Path.cwd() curdir = os.getcwd()
doc.SaveAs(f"{curdir}/{self.filename}.pdf", FileFormat=17) doc.SaveAs(f"{curdir}/{self.filename}.pdf", FileFormat=17)
doc.Close() doc.Close()
word.Quit() word.Quit()

View File

@@ -1,2 +1,2 @@
# import basic error classes # import basic error classes
from .DatabaseErrors import * # noqa: F403 from .DatabaseErrors import *

View File

@@ -6,11 +6,3 @@ from .transformers import (
RDSData, RDSData,
RISData, RISData,
) )
# Explicit re-exports to avoid F401 warnings
RDS_AVAIL_DATA = RDS_AVAIL_DATA
ARRAYData = ARRAYData
BibTeXData = BibTeXData
COinSData = COinSData
RDSData = RDSData
RISData = RISData

View File

@@ -141,7 +141,7 @@ class ARRAYData:
source = source.replace("\t", "").replace("\r", "") source = source.replace("\t", "").replace("\r", "")
source = source.split(search)[1].split(")")[0] source = source.split(search)[1].split(")")[0]
return _get_line(source, entry).replace("=>", "").strip() return _get_line(source, entry).replace("=>", "").strip()
except Exception: except:
return "" return ""
def _get_isbn(source: str) -> list: def _get_isbn(source: str) -> list:
@@ -157,7 +157,7 @@ class ARRAYData:
continue continue
ret.append(isb) if isb not in ret else None ret.append(isb) if isb not in ret else None
return ret return ret
except Exception: except:
isbn = [] isbn = []
return isbn return isbn
@@ -294,7 +294,7 @@ class COinSData:
try: try:
data = source.split(f"{search}=")[1] # .split("")[0].strip() data = source.split(f"{search}=")[1] # .split("")[0].strip()
return data.split("rft")[0].strip() if "rft" in data else data return data.split("rft")[0].strip() if "rft" in data else data
except Exception: except:
return "" return ""
return BookData( return BookData(
@@ -319,7 +319,7 @@ class RISData:
try: try:
data = source.split(f"{search} - ")[1] # .split("")[0].strip() data = source.split(f"{search} - ")[1] # .split("")[0].strip()
return data.split("\n")[0].strip() if "\n" in data else data return data.split("\n")[0].strip() if "\n" in data else data
except Exception: except:
return "" return ""
return BookData( return BookData(
@@ -356,7 +356,7 @@ class BibTeXData:
.replace("[", "") .replace("[", "")
.replace("];", "") .replace("];", "")
) )
except Exception: except:
return "" return ""
return BookData( return BookData(

View File

@@ -9,11 +9,11 @@ from ratelimit import limits, sleep_and_retry
from src.core.models import BookData from src.core.models import BookData
from src.shared.logging import log, get_bloat_logger, preview from src.shared.logging import log, get_bloat_logger, preview
from src.transformers import ARRAYData, BibTeXData, COinSData, RDSData, RISData
from src.transformers.transformers import RDS_AVAIL_DATA, RDS_GENERIC_DATA
# bloat logger for large/raw HTTP responses # bloat logger for large/raw HTTP responses
bloat = get_bloat_logger() bloat = get_bloat_logger()
from src.transformers import ARRAYData, BibTeXData, COinSData, RDSData, RISData
from src.transformers.transformers import RDS_AVAIL_DATA, RDS_GENERIC_DATA
# logger.add(sys.stderr, format="{time} {level} {message}", level="INFO") # logger.add(sys.stderr, format="{time} {level} {message}", level="INFO")

View File

@@ -6,11 +6,3 @@ from .transformers import (
RDSData, RDSData,
RISData, RISData,
) )
# Explicit re-exports to avoid F401 warnings
RDS_AVAIL_DATA = RDS_AVAIL_DATA
ARRAYData = ARRAYData
BibTeXData = BibTeXData
COinSData = COinSData
RDSData = RDSData
RISData = RISData

View File

@@ -141,7 +141,7 @@ class ARRAYData:
source = source.replace("\t", "").replace("\r", "") source = source.replace("\t", "").replace("\r", "")
source = source.split(search)[1].split(")")[0] source = source.split(search)[1].split(")")[0]
return _get_line(source, entry).replace("=>", "").strip() return _get_line(source, entry).replace("=>", "").strip()
except Exception: except:
return "" return ""
def _get_isbn(source: str) -> list: def _get_isbn(source: str) -> list:
@@ -157,7 +157,7 @@ class ARRAYData:
continue continue
ret.append(isb) if isb not in ret else None ret.append(isb) if isb not in ret else None
return ret return ret
except Exception: except:
isbn = [] isbn = []
return isbn return isbn
@@ -294,7 +294,7 @@ class COinSData:
try: try:
data = source.split(f"{search}=")[1] # .split("")[0].strip() data = source.split(f"{search}=")[1] # .split("")[0].strip()
return data.split("rft")[0].strip() if "rft" in data else data return data.split("rft")[0].strip() if "rft" in data else data
except Exception: except:
return "" return ""
return BookData( return BookData(
@@ -319,7 +319,7 @@ class RISData:
try: try:
data = source.split(f"{search} - ")[1] # .split("")[0].strip() data = source.split(f"{search} - ")[1] # .split("")[0].strip()
return data.split("\n")[0].strip() if "\n" in data else data return data.split("\n")[0].strip() if "\n" in data else data
except Exception: except:
return "" return ""
return BookData( return BookData(
@@ -356,7 +356,7 @@ class BibTeXData:
.replace("[", "") .replace("[", "")
.replace("];", "") .replace("];", "")
) )
except Exception: except:
return "" return ""
return BookData( return BookData(

View File

@@ -2,9 +2,6 @@ import pathlib
from .semesterapparat_ui_ui import Ui_MainWindow as Ui_Semesterapparat from .semesterapparat_ui_ui import Ui_MainWindow as Ui_Semesterapparat
# Explicit re-export to avoid F401 warnings
Ui_Semesterapparat = Ui_Semesterapparat
# from .dialogs import ( # from .dialogs import (
# ApparatExtendDialog, # ApparatExtendDialog,
# Mail_Dialog, # Mail_Dialog,

View File

@@ -1,4 +1 @@
from .newMailTemplateDesigner_ui import Ui_Dialog as NewMailTemplateDesignerDialog from .newMailTemplateDesigner_ui import Ui_Dialog as NewMailTemplateDesignerDialog
# Explicit re-export to avoid F401 warnings
NewMailTemplateDesignerDialog = NewMailTemplateDesignerDialog

View File

@@ -9,12 +9,6 @@ from src.services.lehmanns import LehmannsClient
from src.services.sru import SWB from src.services.sru import SWB
def filter_prefer_swb(response):
"""Filter function to prefer SWB results when available."""
# This is a placeholder implementation - adjust based on actual requirements
return response
class CheckThread(QtCore.QThread): class CheckThread(QtCore.QThread):
updateSignal = QtCore.Signal() updateSignal = QtCore.Signal()
total_entries_signal = QtCore.Signal(int) total_entries_signal = QtCore.Signal(int)

View File

@@ -180,8 +180,7 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat):
# self.update_app_media_list() # self.update_app_media_list()
self.populate_prof_dropdown() self.populate_prof_dropdown()
self.populate_appfach_dropdown() self.populate_appfach_dropdown()
# if the focus is changed from the prof name dropdown, set the prof data if the # if the focus is changed from the prof name dropdown, set the prof data if the prof exists in the database, otherwise show a message
# prof exists in the database, otherwise show a message
self.drpdwn_prof_name.currentIndexChanged.connect(self.set_prof_data) # type:ignore self.drpdwn_prof_name.currentIndexChanged.connect(self.set_prof_data) # type:ignore
self.cancel_active_selection.clicked.connect(self.btn_cancel_active_selection) # type:ignore self.cancel_active_selection.clicked.connect(self.btn_cancel_active_selection) # type:ignore
self.check_eternal_app.stateChanged.connect(self.set_state) # type:ignore self.check_eternal_app.stateChanged.connect(self.set_state) # type:ignore
@@ -1244,28 +1243,25 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat):
self.active_apparat, self.active_apparat,
filetype=file_type, filetype=file_type,
) )
if file_type == "pdf": match file_type:
# TODO: implement parser here case "pdf":
data = pdf_to_semap(file) # TODO: implement parser here
signatures = data.signatures data = pdf_to_semap(file)
# self.confirm_popup("PDF Dateien werden nicht unterstützt!", title="Fehler") signatures = data.signatures
return signatures # self.confirm_popup("PDF Dateien werden nicht unterstützt!", title="Fehler")
if file_type == "csv": return signatures
return csv_to_list(file) case "csv":
if file_type in ("docx", "doc"): return csv_to_list(file)
data = word_to_semap(file) case _ if file_type in ("docx", "doc"):
log.info("Converted data from semap file") return word_to_semap(file)
log.debug("Got the data: {}", data) case "eml":
data = eml_to_semap(file)
return data log.info("Converted data from eml file")
if file_type == "eml": log.debug("Got the data: {}", data)
data = eml_to_semap(file) return data
log.info("Converted data from eml file") case _:
log.debug("Got the data: {}", data) error = "Dateityp wird nicht unterstützt"
raise ValueError(error)
return data
error = "Dateityp wird nicht unterstützt"
raise ValueError(error)
def import_data_from_document(self): def import_data_from_document(self):
global valid_input global valid_input
@@ -1315,6 +1311,7 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat):
self.app_fach.setCurrentText(data.subject if data.subject in subjects else "") self.app_fach.setCurrentText(data.subject if data.subject in subjects else "")
self.prof_title.setText(data.personTitle) self.prof_title.setText(data.personTitle)
self.drpdwn_prof_name.setCurrentText(data.personName) self.drpdwn_prof_name.setCurrentText(data.personName)
self.sem_year.setText("20" + str(data.semester.year)) self.sem_year.setText("20" + str(data.semester.year))
if data.semester.semester == "SoSe": if data.semester.semester == "SoSe":
self.sem_sommer.setChecked(True) self.sem_sommer.setChecked(True)
@@ -1326,9 +1323,13 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat):
self.check_eternal_app.setChecked(True) self.check_eternal_app.setChecked(True)
self.validate_semester() self.validate_semester()
if data.books != []: if data.books != []:
log.info("Importing books from document")
log.info(data)
self.btn_check_file_threaded(data) self.btn_check_file_threaded(data)
def btn_check_file_threaded(self, c_document: Optional[SemapDocument] = None): def btn_check_file_threaded(self, c_document: Optional[SemapDocument] = None):
log.info("Starting threaded file check")
log.info(c_document)
for runner in self.bookGrabber: for runner in self.bookGrabber:
if not runner.isRunning(): if not runner.isRunning():
runner.deleteLater() runner.deleteLater()
@@ -1374,8 +1375,7 @@ class Ui(QtWidgets.QMainWindow, Ui_Semesterapparat):
prof_id = self.db.getProfId(self.profdata) prof_id = self.db.getProfId(self.profdata)
# log.debug("Prof ID is None", prof_id) # log.debug("Prof ID is None", prof_id)
document = None document = c_document
if c_document is None: if c_document is None:
document = self.extract_document_data() document = self.extract_document_data()
if document is None: if document is None:

View File

@@ -4,36 +4,15 @@ from src.database import Database
from src.utils.icon import Icon from src.utils.icon import Icon
class AdminQueryWidget(QtWidgets.QWidget): class AdminQueryWidget(QtWidgets.QWidget, Ui_Form):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.setupUi() self.setupUi(self)
self.setWindowIcon(Icon("db_search").icon) self.setWindowIcon(Icon("db_search").icon)
self.db = Database() self.db = Database()
# Connect the button click to the method # Connect the button click to the method
self.sendquery.clicked.connect(self.on_pushButton_clicked) self.sendquery.clicked.connect(self.on_pushButton_clicked)
def setupUi(self):
# Create the layout and widgets
layout = QtWidgets.QVBoxLayout(self)
# Create SQL query input area
self.sqlquery = QtWidgets.QTextEdit()
self.sqlquery.setPlaceholderText("Enter SQL query here...")
# Create execute button
self.sendquery = QtWidgets.QPushButton("Execute Query")
# Create results table
self.queryResult = QtWidgets.QTableWidget()
self.queryResult.setColumnCount(5) # Adjust as needed
self.queryResult.setHorizontalHeaderLabels(["Column 1", "Column 2", "Column 3", "Column 4", "Column 5"])
# Add widgets to layout
layout.addWidget(self.sqlquery)
layout.addWidget(self.sendquery)
layout.addWidget(self.queryResult)
def on_pushButton_clicked(self): def on_pushButton_clicked(self):
# Handle button click event # Handle button click event
self.queryResult.setRowCount(0) # Clear previous results self.queryResult.setRowCount(0) # Clear previous results

View File

@@ -1,13 +1,11 @@
"""Utilities for managing documentation server functionality."""
import logging import logging
import os
import subprocess import subprocess
import sys import sys
from pathlib import Path
from src import LOG_DIR from src import LOG_DIR
log_path = Path(LOG_DIR) / "web_documentation.log" log_path = os.path.join(LOG_DIR, "web_documentation.log")
# Replace the default StreamHandler with a FileHandler # Replace the default StreamHandler with a FileHandler
logging.basicConfig( logging.basicConfig(
@@ -21,12 +19,13 @@ logger = logging.getLogger(__name__) # inherits the same file handler
docport = 8000 docport = 8000
def start_documentation_server(): def start_documentation_server():
"""Start the Zensical documentation server as a subprocess. """
Start the Zensical documentation server as a subprocess.
Returns: Returns:
subprocess.Popen: The subprocess object, or None if startup failed. subprocess.Popen: The subprocess object, or None if startup failed.
""" """
try: try:
# Prepare subprocess arguments # Prepare subprocess arguments
@@ -42,7 +41,7 @@ def start_documentation_server():
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
stdin=subprocess.DEVNULL, stdin=subprocess.DEVNULL,
creationflags=creationflags, creationflags=creationflags,
cwd=Path.cwd(), cwd=os.getcwd(),
) )
logger.info(f"Documentation server started with PID {process.pid}") logger.info(f"Documentation server started with PID {process.pid}")

View File

@@ -1,7 +1,6 @@
import os import os
from datetime import datetime from datetime import datetime
from os.path import basename from os.path import basename
from pathlib import Path
from docx import Document from docx import Document
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
@@ -70,20 +69,20 @@ class SemesterDocument:
full: bool = False, full: bool = False,
): ):
assert isinstance(apparats, list), SemesterError( assert isinstance(apparats, list), SemesterError(
"Apparats must be a list of tuples", "Apparats must be a list of tuples"
) )
assert all(isinstance(apparat, tuple) for apparat in apparats), SemesterError( assert all(isinstance(apparat, tuple) for apparat in apparats), SemesterError(
"Apparats must be a list of tuples", "Apparats must be a list of tuples"
) )
assert all(isinstance(apparat[0], int) for apparat in apparats), SemesterError( assert all(isinstance(apparat[0], int) for apparat in apparats), SemesterError(
"Apparat numbers must be integers", "Apparat numbers must be integers"
) )
assert all(isinstance(apparat[1], str) for apparat in apparats), SemesterError( assert all(isinstance(apparat[1], str) for apparat in apparats), SemesterError(
"Apparat names must be strings", "Apparat names must be strings"
) )
assert isinstance(semester, str), SemesterError("Semester must be a string") assert isinstance(semester, str), SemesterError("Semester must be a string")
assert "." not in filename and isinstance(filename, str), SemesterError( assert "." not in filename and isinstance(filename, str), SemesterError(
"Filename must be a string and not contain an extension", "Filename must be a string and not contain an extension"
) )
self.doc = Document() self.doc = Document()
self.apparats = apparats self.apparats = apparats
@@ -109,7 +108,8 @@ class SemesterDocument:
log.info("Document printed") log.info("Document printed")
def set_table_border(self, table): def set_table_border(self, table):
"""Adds a full border to the table. """
Adds a full border to the table.
:param table: Table object to which the border will be applied. :param table: Table object to which the border will be applied.
""" """
@@ -150,8 +150,7 @@ class SemesterDocument:
trPr = row._tr.get_or_add_trPr() # Get or add the <w:trPr> element trPr = row._tr.get_or_add_trPr() # Get or add the <w:trPr> element
trHeight = OxmlElement("w:trHeight") trHeight = OxmlElement("w:trHeight")
trHeight.set( trHeight.set(
qn("w:val"), qn("w:val"), str(int(Pt(15).pt * 20))
str(int(Pt(15).pt * 20)),
) # Convert points to twips ) # Convert points to twips
trHeight.set(qn("w:hRule"), "exact") # Use "exact" for fixed height trHeight.set(qn("w:hRule"), "exact") # Use "exact" for fixed height
trPr.append(trHeight) trPr.append(trHeight)
@@ -234,7 +233,7 @@ class SemesterDocument:
self.save_document(self.filename + ".docx") self.save_document(self.filename + ".docx")
docpath = os.path.abspath(self.filename + ".docx") docpath = os.path.abspath(self.filename + ".docx")
doc = word.Documents.Open(docpath) doc = word.Documents.Open(docpath)
curdir = Path.cwd() curdir = os.getcwd()
doc.SaveAs(f"{curdir}/{self.filename}.pdf", FileFormat=17) doc.SaveAs(f"{curdir}/{self.filename}.pdf", FileFormat=17)
doc.Close() doc.Close()
word.Quit() word.Quit()
@@ -318,7 +317,7 @@ class SemapSchilder:
self.save_document() self.save_document()
docpath = os.path.abspath(f"{self.filename}.docx") docpath = os.path.abspath(f"{self.filename}.docx")
doc = word.Documents.Open(docpath) doc = word.Documents.Open(docpath)
curdir = Path.cwd() curdir = os.getcwd()
doc.SaveAs(f"{curdir}/{self.filename}.pdf", FileFormat=17) doc.SaveAs(f"{curdir}/{self.filename}.pdf", FileFormat=17)
doc.Close() doc.Close()
word.Quit() word.Quit()

14
test.py
View File

@@ -1,12 +1,10 @@
# from src.services.webrequest import BibTextTransformer, TransformerType, WebRequest from src.services.webrequest import BibTextTransformer, TransformerType, WebRequest
# transformer = BibTextTransformer(TransformerType.RDS) transformer = BibTextTransformer(TransformerType.RDS)
# data = WebRequest().set_apparat(71) data = WebRequest().set_apparat(71)
# data = data.get_ppn("CU 3700 R244 (2)").get_data() data = data.get_ppn("CU 3700 R244 (2)").get_data()
# rds = transformer.get_data(data).return_data("rds_availability") rds = transformer.get_data(data).return_data("rds_availability")
# print(rds)
from src.parsers.xml_parser import eml_parser, eml_to_semap
print(rds)

163
uv.lock generated
View File

@@ -307,75 +307,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/03/2f/ca9029d5da14b5a3a103d6061149a4a94a54ab848f56c7d2809dbb36f48c/comtypes-1.4.15-py3-none-any.whl", hash = "sha256:cda90486de8762ec57d7ce04e68721920911f3f03415cb29afdf7609c427c7e3", size = 274650, upload-time = "2026-01-19T23:45:44.34Z" }, { url = "https://files.pythonhosted.org/packages/03/2f/ca9029d5da14b5a3a103d6061149a4a94a54ab848f56c7d2809dbb36f48c/comtypes-1.4.15-py3-none-any.whl", hash = "sha256:cda90486de8762ec57d7ce04e68721920911f3f03415cb29afdf7609c427c7e3", size = 274650, upload-time = "2026-01-19T23:45:44.34Z" },
] ]
[[package]]
name = "coverage"
version = "7.13.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" },
{ url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" },
{ url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" },
{ url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" },
{ url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" },
{ url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" },
{ url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" },
{ url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" },
{ url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" },
{ url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" },
{ url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" },
{ url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" },
{ url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" },
{ url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" },
{ url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" },
{ url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" },
{ url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" },
{ url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" },
{ url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" },
{ url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" },
{ url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" },
{ url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" },
{ url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" },
{ url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" },
{ url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" },
{ url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" },
{ url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" },
{ url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" },
{ url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" },
{ url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" },
{ url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" },
{ url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" },
{ url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" },
{ url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" },
{ url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" },
{ url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" },
{ url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" },
{ url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" },
{ url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" },
{ url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" },
{ url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" },
{ url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" },
{ url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" },
{ url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" },
{ url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" },
{ url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" },
{ url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" },
{ url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" },
{ url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" },
{ url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" },
{ url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" },
{ url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" },
{ url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" },
{ url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" },
{ url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" },
{ url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" },
{ url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" },
{ url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" },
{ url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" },
{ url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" },
{ url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" },
]
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "46.0.4" version = "46.0.4"
@@ -621,15 +552,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
] ]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]] [[package]]
name = "itsdangerous" name = "itsdangerous"
version = "2.2.0" version = "2.2.0"
@@ -1077,12 +999,27 @@ wheels = [
] ]
[[package]] [[package]]
name = "pluggy" name = "prek"
version = "1.6.0" version = "0.3.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } sdist = { url = "https://files.pythonhosted.org/packages/d3/f5/ee52def928dd1355c20bcfcf765e1e61434635c33f3075e848e7b83a157b/prek-0.3.2.tar.gz", hash = "sha256:dce0074ff1a21290748ca567b4bda7553ee305a8c7b14d737e6c58364a499364", size = 334229, upload-time = "2026-02-06T13:49:47.539Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, { url = "https://files.pythonhosted.org/packages/76/69/70a5fc881290a63910494df2677c0fb241d27cfaa435bbcd0de5cd2e2443/prek-0.3.2-py3-none-linux_armv6l.whl", hash = "sha256:4f352f9c3fc98aeed4c8b2ec4dbf16fc386e45eea163c44d67e5571489bd8e6f", size = 4614960, upload-time = "2026-02-06T13:50:05.818Z" },
{ url = "https://files.pythonhosted.org/packages/c0/15/a82d5d32a2207ccae5d86ea9e44f2b93531ed000faf83a253e8d1108e026/prek-0.3.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4a000cfbc3a6ec7d424f8be3c3e69ccd595448197f92daac8652382d0acc2593", size = 4622889, upload-time = "2026-02-06T13:49:53.662Z" },
{ url = "https://files.pythonhosted.org/packages/89/75/ea833b58a12741397017baef9b66a6e443bfa8286ecbd645d14111446280/prek-0.3.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5436bdc2702cbd7bcf9e355564ae66f8131211e65fefae54665a94a07c3d450a", size = 4239653, upload-time = "2026-02-06T13:50:02.88Z" },
{ url = "https://files.pythonhosted.org/packages/10/b4/d9c3885987afac6e20df4cb7db14e3b0d5a08a77ae4916488254ebac4d0b/prek-0.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:0161b5f584f9e7f416d6cf40a17b98f17953050ff8d8350ec60f20fe966b86b6", size = 4595101, upload-time = "2026-02-06T13:49:49.813Z" },
{ url = "https://files.pythonhosted.org/packages/21/a6/1a06473ed83dbc898de22838abdb13954e2583ce229f857f61828384634c/prek-0.3.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4e641e8533bca38797eebb49aa89ed0e8db0e61225943b27008c257e3af4d631", size = 4521978, upload-time = "2026-02-06T13:49:41.266Z" },
{ url = "https://files.pythonhosted.org/packages/0c/5e/c38390d5612e6d86b32151c1d2fdab74a57913473193591f0eb00c894c21/prek-0.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfca1810d49d3f9ef37599c958c4e716bc19a1d78a7e88cbdcb332e0b008994f", size = 4829108, upload-time = "2026-02-06T13:49:44.598Z" },
{ url = "https://files.pythonhosted.org/packages/80/a6/cecce2ab623747ff65ed990bb0d95fa38449ee19b348234862acf9392fff/prek-0.3.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d69d754299a95a85dc20196f633232f306bee7e7c8cba61791f49ce70404ec", size = 5357520, upload-time = "2026-02-06T13:49:48.512Z" },
{ url = "https://files.pythonhosted.org/packages/a5/18/d6bcb29501514023c76d55d5cd03bdbc037737c8de8b6bc41cdebfb1682c/prek-0.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:539dcb90ad9b20837968539855df6a29493b328a1ae87641560768eed4f313b0", size = 4852635, upload-time = "2026-02-06T13:49:58.347Z" },
{ url = "https://files.pythonhosted.org/packages/1b/0a/ae46f34ba27ba87aea5c9ad4ac9cd3e07e014fd5079ae079c84198f62118/prek-0.3.2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:1998db3d0cbe243984736c82232be51318f9192e2433919a6b1c5790f600b5fd", size = 4599484, upload-time = "2026-02-06T13:49:43.296Z" },
{ url = "https://files.pythonhosted.org/packages/1a/a9/73bfb5b3f7c3583f9b0d431924873928705cdef6abb3d0461c37254a681b/prek-0.3.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:07ab237a5415a3e8c0db54de9d63899bcd947624bdd8820d26f12e65f8d19eb7", size = 4657694, upload-time = "2026-02-06T13:50:01.074Z" },
{ url = "https://files.pythonhosted.org/packages/a7/bc/0994bc176e1a80110fad3babce2c98b0ac4007630774c9e18fc200a34781/prek-0.3.2-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:0ced19701d69c14a08125f14a5dd03945982edf59e793c73a95caf4697a7ac30", size = 4509337, upload-time = "2026-02-06T13:49:54.891Z" },
{ url = "https://files.pythonhosted.org/packages/f9/13/e73f85f65ba8f626468e5d1694ab3763111513da08e0074517f40238c061/prek-0.3.2-py3-none-musllinux_1_1_i686.whl", hash = "sha256:ffb28189f976fa111e770ee94e4f298add307714568fb7d610c8a7095cb1ce59", size = 4697350, upload-time = "2026-02-06T13:50:04.526Z" },
{ url = "https://files.pythonhosted.org/packages/14/47/98c46dcd580305b9960252a4eb966f1a7b1035c55c363f378d85662ba400/prek-0.3.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f63134b3eea14421789a7335d86f99aee277cb520427196f2923b9260c60e5c5", size = 4955860, upload-time = "2026-02-06T13:49:56.581Z" },
{ url = "https://files.pythonhosted.org/packages/73/42/1bb4bba3ff47897df11e9dfd774027cdfa135482c961a54e079af0faf45a/prek-0.3.2-py3-none-win32.whl", hash = "sha256:58c806bd1344becd480ef5a5ba348846cc000af0e1fbe854fef91181a2e06461", size = 4267619, upload-time = "2026-02-06T13:49:39.503Z" },
{ url = "https://files.pythonhosted.org/packages/97/11/6665f47a7c350d83de17403c90bbf7a762ef50876ece456a86f64f46fbfb/prek-0.3.2-py3-none-win_amd64.whl", hash = "sha256:70114b48e9eb8048b2c11b4c7715ce618529c6af71acc84dd8877871a2ef71a6", size = 4624324, upload-time = "2026-02-06T13:49:45.922Z" },
{ url = "https://files.pythonhosted.org/packages/22/e7/740997ca82574d03426f897fd88afe3fc8a7306b8c7ea342a8bc1c538488/prek-0.3.2-py3-none-win_arm64.whl", hash = "sha256:9144d176d0daa2469a25c303ef6f6fa95a8df015eb275232f5cb53551ecefef0", size = 4336008, upload-time = "2026-02-06T13:49:52.27Z" },
] ]
[[package]] [[package]]
@@ -1348,36 +1285,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b9/f9/c9757a984c4ffb6d12fab69e966d95dfc862a5d44e12b7900f3a03780b76/pyside6_essentials-6.10.2-cp39-abi3-win_arm64.whl", hash = "sha256:db5f4913648bb6afddb8b347edae151ee2378f12bceb03c8b2515a530a4b38d9", size = 55258626, upload-time = "2026-02-02T08:46:36.788Z" }, { url = "https://files.pythonhosted.org/packages/b9/f9/c9757a984c4ffb6d12fab69e966d95dfc862a5d44e12b7900f3a03780b76/pyside6_essentials-6.10.2-cp39-abi3-win_arm64.whl", hash = "sha256:db5f4913648bb6afddb8b347edae151ee2378f12bceb03c8b2515a530a4b38d9", size = 55258626, upload-time = "2026-02-02T08:46:36.788Z" },
] ]
[[package]]
name = "pytest"
version = "9.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
]
[[package]]
name = "pytest-cov"
version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coverage" },
{ name = "pluggy" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
]
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.9.0.post0" version = "2.9.0.post0"
@@ -1664,10 +1571,8 @@ dev = [
{ name = "bump-my-version" }, { name = "bump-my-version" },
{ name = "icecream" }, { name = "icecream" },
{ name = "nuitka" }, { name = "nuitka" },
{ name = "prek" },
{ name = "pyinstaller" }, { name = "pyinstaller" },
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "ty" },
] ]
swbtest = [ swbtest = [
{ name = "alive-progress" }, { name = "alive-progress" },
@@ -1708,10 +1613,8 @@ dev = [
{ name = "bump-my-version", specifier = ">=0.29.0" }, { name = "bump-my-version", specifier = ">=0.29.0" },
{ name = "icecream", specifier = ">=2.1.4" }, { name = "icecream", specifier = ">=2.1.4" },
{ name = "nuitka", specifier = ">=2.5.9" }, { name = "nuitka", specifier = ">=2.5.9" },
{ name = "prek", specifier = ">=0.3.2" },
{ name = "pyinstaller", specifier = ">=6.17.0" }, { name = "pyinstaller", specifier = ">=6.17.0" },
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "ty", specifier = ">=0.0.15" },
] ]
swbtest = [{ name = "alive-progress", specifier = ">=3.3.0" }] swbtest = [{ name = "alive-progress", specifier = ">=3.3.0" }]
@@ -1790,30 +1693,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
] ]
[[package]]
name = "ty"
version = "0.0.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4e/25/257602d316b9333089b688a7a11b33ebc660b74e8dacf400dc3dfdea1594/ty-0.0.15.tar.gz", hash = "sha256:4f9a5b8df208c62dba56e91b93bed8b5bb714839691b8cff16d12c983bfa1174", size = 5101936, upload-time = "2026-02-05T01:06:34.922Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/c5/35626e732b79bf0e6213de9f79aff59b5f247c0a1e3ce0d93e675ab9b728/ty-0.0.15-py3-none-linux_armv6l.whl", hash = "sha256:68e092458516c61512dac541cde0a5e4e5842df00b4e81881ead8f745ddec794", size = 10138374, upload-time = "2026-02-05T01:07:03.804Z" },
{ url = "https://files.pythonhosted.org/packages/d5/8a/48fd81664604848f79d03879b3ca3633762d457a069b07e09fb1b87edd6e/ty-0.0.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:79f2e75289eae3cece94c51118b730211af4ba5762906f52a878041b67e54959", size = 9947858, upload-time = "2026-02-05T01:06:47.453Z" },
{ url = "https://files.pythonhosted.org/packages/b6/85/c1ac8e97bcd930946f4c94db85b675561d590b4e72703bf3733419fc3973/ty-0.0.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:112a7b26e63e48cc72c8c5b03227d1db280cfa57a45f2df0e264c3a016aa8c3c", size = 9443220, upload-time = "2026-02-05T01:06:44.98Z" },
{ url = "https://files.pythonhosted.org/packages/3c/d9/244bc02599d950f7a4298fbc0c1b25cc808646b9577bdf7a83470b2d1cec/ty-0.0.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71f62a2644972975a657d9dc867bf901235cde51e8d24c20311067e7afd44a56", size = 9949976, upload-time = "2026-02-05T01:07:01.515Z" },
{ url = "https://files.pythonhosted.org/packages/7e/ab/3a0daad66798c91a33867a3ececf17d314ac65d4ae2bbbd28cbfde94da63/ty-0.0.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e48b42be2d257317c85b78559233273b655dd636fc61e7e1d69abd90fd3cba4", size = 9965918, upload-time = "2026-02-05T01:06:54.283Z" },
{ url = "https://files.pythonhosted.org/packages/39/4e/e62b01338f653059a7c0cd09d1a326e9a9eedc351a0f0de9db0601658c3d/ty-0.0.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27dd5b52a421e6871c5bfe9841160331b60866ed2040250cb161886478ab3e4f", size = 10424943, upload-time = "2026-02-05T01:07:08.777Z" },
{ url = "https://files.pythonhosted.org/packages/65/b5/7aa06655ce69c0d4f3e845d2d85e79c12994b6d84c71699cfb437e0bc8cf/ty-0.0.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76b85c9ec2219e11c358a7db8e21b7e5c6674a1fb9b6f633836949de98d12286", size = 10964692, upload-time = "2026-02-05T01:06:37.103Z" },
{ url = "https://files.pythonhosted.org/packages/13/04/36fdfe1f3c908b471e246e37ce3d011175584c26d3853e6c5d9a0364564c/ty-0.0.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9e8204c61d8ede4f21f2975dce74efdb80fafb2fae1915c666cceb33ea3c90b", size = 10692225, upload-time = "2026-02-05T01:06:49.714Z" },
{ url = "https://files.pythonhosted.org/packages/13/41/5bf882649bd8b64ded5fbce7fb8d77fb3b868de1a3b1a6c4796402b47308/ty-0.0.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af87c3be7c944bb4d6609d6c63e4594944b0028c7bd490a525a82b88fe010d6d", size = 10516776, upload-time = "2026-02-05T01:06:52.047Z" },
{ url = "https://files.pythonhosted.org/packages/56/75/66852d7e004f859839c17ffe1d16513c1e7cc04bcc810edb80ca022a9124/ty-0.0.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:50dccf7398505e5966847d366c9e4c650b8c225411c2a68c32040a63b9521eea", size = 9928828, upload-time = "2026-02-05T01:06:56.647Z" },
{ url = "https://files.pythonhosted.org/packages/65/72/96bc16c7b337a3ef358fd227b3c8ef0c77405f3bfbbfb59ee5915f0d9d71/ty-0.0.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:bd797b8f231a4f4715110259ad1ad5340a87b802307f3e06d92bfb37b858a8f3", size = 9978960, upload-time = "2026-02-05T01:06:29.567Z" },
{ url = "https://files.pythonhosted.org/packages/a0/18/d2e316a35b626de2227f832cd36d21205e4f5d96fd036a8af84c72ecec1b/ty-0.0.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9deb7f20e18b25440a9aa4884f934ba5628ef456dbde91819d5af1a73da48af3", size = 10135903, upload-time = "2026-02-05T01:06:59.256Z" },
{ url = "https://files.pythonhosted.org/packages/02/d3/b617a79c9dad10c888d7c15cd78859e0160b8772273637b9c4241a049491/ty-0.0.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7b31b3de031255b90a5f4d9cb3d050feae246067c87130e5a6861a8061c71754", size = 10615879, upload-time = "2026-02-05T01:07:06.661Z" },
{ url = "https://files.pythonhosted.org/packages/fb/b0/2652a73c71c77296a6343217063f05745da60c67b7e8a8e25f2064167fce/ty-0.0.15-py3-none-win32.whl", hash = "sha256:9362c528ceb62c89d65c216336d28d500bc9f4c10418413f63ebc16886e16cc1", size = 9578058, upload-time = "2026-02-05T01:06:42.928Z" },
{ url = "https://files.pythonhosted.org/packages/84/6e/08a4aedebd2a6ce2784b5bc3760e43d1861f1a184734a78215c2d397c1df/ty-0.0.15-py3-none-win_amd64.whl", hash = "sha256:4db040695ae67c5524f59cb8179a8fa277112e69042d7dfdac862caa7e3b0d9c", size = 10457112, upload-time = "2026-02-05T01:06:39.885Z" },
{ url = "https://files.pythonhosted.org/packages/b3/be/1991f2bc12847ae2d4f1e3ac5dcff8bb7bc1261390645c0755bb55616355/ty-0.0.15-py3-none-win_arm64.whl", hash = "sha256:e5a98d4119e77d6136461e16ae505f8f8069002874ab073de03fbcb1a5e8bf25", size = 9937490, upload-time = "2026-02-05T01:06:32.388Z" },
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.15.0" version = "4.15.0"