fix: issues (1)

This commit is contained in:
2026-02-12 08:54:19 +01:00
parent 8ec92a685c
commit d316601e9a
27 changed files with 440 additions and 235 deletions

View File

@@ -373,26 +373,31 @@ 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
```bash ## 📚 Documentation
# Using uv
uv run mkdocs build
uv run mkdocs serve # View at http://localhost:8000
# Or with activated venv - **[User Manual](docs/)**: Complete user guide built with Zola and the Tanuki theme
mkdocs build - View documentation at `http://localhost:8000` when running the application
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
mkdocs build # Build documentation using the provided script
mkdocs serve # View at http://localhost:8000 .\build_docs.ps1
# 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 from omegaconf import OmegaConf, DictConfig, ListConfig
import os import os
from pathlib import Path from pathlib import Path
@@ -140,7 +140,7 @@ class Config:
""" """
_config: Optional[DictConfig] = None _config: Optional[Union[DictConfig, ListConfig]] = None
config_exists: bool = True config_exists: bool = True
def __init__(self, config_path: str): def __init__(self, config_path: str):
@@ -183,22 +183,25 @@ class Config:
""" """
Reloads the configuration from the file. Reloads the configuration from the file.
""" """
self._config = OmegaConf.load(self.config_path) if self.config_path is not None:
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)
@property def get_zotero_attr(self, name: str):
def zotero_attr(self, name: str):
return getattr(self.zotero, name) return getattr(self.zotero, name)
@zotero_attr.setter def set_zotero_attr(self, name: str, value: Any):
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
@@ -211,43 +214,57 @@ 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):
OmegaConf.update(self._config, f"mail.{name}", value) if self._config is not None:
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):
OmegaConf.update(self._config, f"database.{name}", value) if self._config is not None:
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):
OmegaConf.update(self._config, f"zotero.{name}", value) if self._config is not None:
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):
OmegaConf.update(self._config, f"openAI.{name}", value) if self._config is not None:
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):
OmegaConf.update(self._config, f"icons.{name}", value) if self._config is not None:
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):
self._config.save_path = value if self._config is not None:
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 docs/themes/tanuki deleted from f81db54c4e

View File

@@ -41,34 +41,14 @@ 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.bumpversion] [tool.ruff]
current_version = "1.0.2" line-length = 88
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)" target-version = "py313"
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,6 +33,8 @@ 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
@@ -49,8 +51,6 @@ 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: except Exception:
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 * from .constants import * # noqa: F403
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]]): def from_dict(self, data: dict[str, Union[str, int]]) -> 'Prof':
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,27 +38,40 @@ 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[str, int], ...]) -> Prof: def from_tuple(self, data: tuple[Union[int, str, None], ...]) -> 'Prof':
self.id = data[0] self.id = data[0] if data[0] is not None and isinstance(data[0], int) else None
self._title = data[1] self._title = str(data[1]) if data[1] is not None else None
self.firstname = data[2] self.firstname = str(data[2]) if data[2] is not None else None
self.lastname = data[3] self.lastname = str(data[3]) if data[3] is not None else None
self.fullname = data[4] self.fullname = str(data[4]) if data[4] is not None else None
self.mail = data[5] self.mail = str(data[5]) if data[5] is not None else None
self.telnr = data[6] self.telnr = str(data[6]) if data[6] is not None else None
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 "," in self.fullname: if self.fullname and "," in self.fullname:
self.firstname = self.fullname.split(",")[1].strip() parts = self.fullname.split(",")
self.lastname = self.fullname.split(",")[0].strip() if len(parts) >= 2:
self.firstname = parts[1].strip()
self.lastname = parts[0].strip()
else: else:
return self.fullname return self.fullname
if comma: if comma:
return f"{self.lastname}, {self.firstname}" if self.lastname and 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
@@ -90,10 +103,12 @@ 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)
self.year = regex.sub(r"[^\d]", "", str(self.year)) if self.year else None if self.year is not 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) -> BookData: def from_dict(self, data: dict[str, Any]) -> '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
@@ -132,23 +147,26 @@ 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, dataclass: Optional[Any]) -> None: def from_dataclass(self, data_obj: Optional[Any]) -> None:
if dataclass is None: if data_obj is None:
return return
for key, value in dataclass.__dict__.items(): for key, value in data_obj.__dict__.items():
setattr(self, key, value) setattr(self, key, value)
def get_book_type(self) -> str: def get_book_type(self) -> str:
if "Online" in self.pages: if self.pages and "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
return BookData(**ndata) def from_LehmannsSearchResult(self, result: Any) -> 'BookData':
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
@@ -170,7 +188,7 @@ 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) match = regex.search(r"(\d+)", self.edition or "")
if match: if match:
return int(match.group(1)) return int(match.group(1))
return 0 return 0
@@ -215,13 +233,13 @@ class Subjects(Enum):
return self.value[0] return self.value[0]
@property @property
def name(self) -> str: def subject_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.name == name: if i.subject_name == name:
return i.id - 1 return i.id - 1
return None return None
@@ -355,25 +373,24 @@ class Book:
@dataclass @dataclass
class SemapDocument: class SemapDocument:
subject: str | None subject: str | None = None
phoneNumber: int | None phoneNumber: int | None = None
mail: str | None mail: str | None = None
title: str | None title: str | None = None
personName: str | None personName: str | None = None
personTitle: str | None personTitle: str | None = None
title_suggestions: list[str] = None title_suggestions: list[str] = field(default_factory=list)
semester: Union[str, Semester] = None semester: Union[str, 'Semester', None] = None
books: list[Book] = None books: list[Book] = field(default_factory=list)
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:
""".""" """."""
self.title_suggestions = [] if self.phoneNumber is not None:
self.phoneNumber = int( phone_str = regex.sub(r"[^\d]", "", str(self.phoneNumber))
regex.sub(r"[^\d]", "", str(self.phoneNumber)), self.phoneNumber = int(phone_str) if phone_str else None
)
@property @property
def nameSetter(self): def nameSetter(self):
@@ -399,7 +416,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: if self.semester and isinstance(self.semester, str):
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)."""
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Classlevel defaults will be *copied* to each instance and then # Class-level 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 # Humanreadable label, e.g. "WiSe 23/24" value: str | None = None # Human-readable label, e.g. "WiSe 23/24"
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Construction helpers # Construction helpers
@@ -89,17 +89,22 @@ 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."""
self._semester = "WiSe" if (self._month <= 3 or self._month > 9) else "SoSe" if self._month is not None:
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:
"""Humanreadable semester label e.g. ``WiSe 23/24`` or ``SoSe 24``.""" """Human-readable semester label - e.g. ``WiSe 23/24`` or ``SoSe 24``."""
year = self._year if self._year is not None:
if self._semester == "WiSe": year = self._year
next_year = (year + 1) % 100 # wrap 99 → 0 if self._semester == "WiSe":
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}" self.value = f"SoSe {year}"
else:
self.value = "<invalid Semester>"
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Public API # Public API
@@ -117,10 +122,12 @@ 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"
@@ -164,10 +171,14 @@ 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
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -178,14 +189,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.isMatch(end): if start.is_future_semester(end) and not start.is_match(end):
raise ValueError("'start' must not be after 'end'") raise ValueError("'start' must not be after 'end'")
chain: list[Semester] = [start.value] chain: list[str] = [str(start)]
current = start current = start
while not current.isMatch(end): while not current.is_match(end):
current = current.next current = current.next
chain.append(current.value) chain.append(str(current))
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
@@ -195,9 +206,9 @@ class Semester:
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@classmethod @classmethod
def from_string(cls, s: str) -> Semester: def from_string(cls, s: str) -> Semester:
"""Parse a humanreadable semester label and return a :class:`Semester`. """Parse a human-readable semester label and return a :class:`Semester`.
Accepted formats (caseinsensitive):: Accepted formats (case-insensitive)::
"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
@@ -212,7 +223,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()
@@ -236,7 +247,7 @@ class Semester:
return cls(year, "WiSe") return cls(year, "WiSe")
# ------------------------- quick selftest ------------------------- # ------------------------- quick self-test -------------------------
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 Exception as e: except (sql.Error, OSError, IOError) 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, id): def getElsaMediaType(self, media_id):
query = "SELECT type FROM elsa_media WHERE id=?" query = "SELECT type FROM elsa_media WHERE id=?"
return self.query_db(query, (id,), one=True)[0] return self.query_db(query, (media_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 Exception: except (TypeError, UnicodeDecodeError, ValueError):
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, id) -> Prof: def getProf(self, prof_id) -> Prof:
"""Get a professor based on the id """Get a professor based on the id
Args: Args:
id ([type]): the id of the professor prof_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=?", (id,), one=True) data = self.query_db("SELECT * FROM prof WHERE id=?", (prof_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, id): def isEternal(self, apparat_id):
"""check if the apparat is eternal (dauerapparat) """check if the apparat is eternal (dauerapparat)
Args: Args:
id (int): the id of the apparat to be checked apparat_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=?", (id,), one=True "SELECT dauer FROM semesterapparat WHERE appnr=?", (apparat_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,6 +1,7 @@
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
@@ -69,20 +70,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
@@ -108,8 +109,7 @@ 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,7 +150,8 @@ 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"), str(int(Pt(15).pt * 20)) qn("w:val"),
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)
@@ -233,7 +234,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 = os.getcwd() curdir = Path.cwd()
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()
@@ -317,7 +318,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 = os.getcwd() curdir = Path.cwd()
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 * from .DatabaseErrors import * # noqa: F403

View File

@@ -6,3 +6,11 @@ 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: except Exception:
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: except Exception:
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: except Exception:
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: except Exception:
return "" return ""
return BookData( return BookData(
@@ -356,7 +356,7 @@ class BibTeXData:
.replace("[", "") .replace("[", "")
.replace("];", "") .replace("];", "")
) )
except: except Exception:
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,3 +6,11 @@ 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: except Exception:
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: except Exception:
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: except Exception:
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: except Exception:
return "" return ""
return BookData( return BookData(
@@ -356,7 +356,7 @@ class BibTeXData:
.replace("[", "") .replace("[", "")
.replace("];", "") .replace("];", "")
) )
except: except Exception:
return "" return ""
return BookData( return BookData(

View File

@@ -1,32 +1,35 @@
import pathlib import pathlib
from .semesterapparat_ui_ui import Ui_MainWindow as Ui_Semesterapparat from .semesterapparat_ui_ui import Ui_MainWindow as Ui_Semesterapparat
# from .dialogs import ( # Explicit re-export to avoid F401 warnings
# ApparatExtendDialog, Ui_Semesterapparat = Ui_Semesterapparat
# Mail_Dialog,
# Settings, # from .dialogs import (
# edit_bookdata_ui, # ApparatExtendDialog,
# login_ui, # Mail_Dialog,
# medienadder_ui, # Settings,
# parsed_titles_ui, # edit_bookdata_ui,
# popus_confirm, # login_ui,
# reminder_ui, # medienadder_ui,
# About, # parsed_titles_ui,
# ElsaAddEntry, # popus_confirm,
# ) # reminder_ui,
# from .widgets import ( # About,
# FilePicker, # ElsaAddEntry,
# StatusWidget, # )
# CalendarEntry, # from .widgets import (
# MessageCalendar, # FilePicker,
# SearchStatisticPage, # # StatusWidget,
# DataGraph, # CalendarEntry,
# ElsaDialog, # MessageCalendar,
# UserCreate, # SearchStatisticPage, #
# EditUser, # DataGraph,
# EditProf, # ElsaDialog,
# ) # UserCreate,
path = pathlib.Path(__file__).parent.absolute() # EditUser,
# from .mainwindow import Ui_MainWindow as Ui_MainWindow # EditProf,
# from .sap import Ui_MainWindow as MainWindow_SAP # )
path = pathlib.Path(__file__).parent.absolute()
# from .mainwindow import Ui_MainWindow as Ui_MainWindow
# from .sap import Ui_MainWindow as MainWindow_SAP

View File

@@ -1 +1,4 @@
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,6 +9,12 @@ 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,7 +180,8 @@ 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 prof exists in the database, otherwise show a message # 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
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

View File

@@ -4,15 +4,36 @@ from src.database import Database
from src.utils.icon import Icon from src.utils.icon import Icon
class AdminQueryWidget(QtWidgets.QWidget, Ui_Form): class AdminQueryWidget(QtWidgets.QWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.setupUi(self) self.setupUi()
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,11 +1,13 @@
"""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 = os.path.join(LOG_DIR, "web_documentation.log") log_path = Path(LOG_DIR) / "web_documentation.log"
# Replace the default StreamHandler with a FileHandler # Replace the default StreamHandler with a FileHandler
logging.basicConfig( logging.basicConfig(
@@ -19,13 +21,12 @@ 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
@@ -33,7 +34,7 @@ def start_documentation_server():
if sys.platform == "win32": if sys.platform == "win32":
# Hide console window on Windows # Hide console window on Windows
creationflags = subprocess.CREATE_NO_WINDOW creationflags = subprocess.CREATE_NO_WINDOW
# Start subprocess with all output suppressed # Start subprocess with all output suppressed
process = subprocess.Popen( process = subprocess.Popen(
["uv", "run", "zensical", "serve"], ["uv", "run", "zensical", "serve"],
@@ -41,12 +42,12 @@ def start_documentation_server():
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
stdin=subprocess.DEVNULL, stdin=subprocess.DEVNULL,
creationflags=creationflags, creationflags=creationflags,
cwd=os.getcwd(), cwd=Path.cwd(),
) )
logger.info(f"Documentation server started with PID {process.pid}") logger.info(f"Documentation server started with PID {process.pid}")
return process return process
except Exception as e: except Exception as e:
logger.error(f"Failed to start documentation server: {e}") logger.error(f"Failed to start documentation server: {e}")
return None return None

View File

@@ -1,6 +1,7 @@
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
@@ -69,20 +70,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
@@ -108,8 +109,7 @@ 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,7 +150,8 @@ 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"), str(int(Pt(15).pt * 20)) qn("w:val"),
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)
@@ -233,7 +234,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 = os.getcwd() curdir = Path.cwd()
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()
@@ -317,7 +318,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 = os.getcwd() curdir = Path.cwd()
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,10 +1,12 @@
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,6 +307,75 @@ 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"
@@ -552,6 +621,15 @@ 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"
@@ -999,27 +1077,12 @@ wheels = [
] ]
[[package]] [[package]]
name = "prek" name = "pluggy"
version = "0.3.2" version = "1.6.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
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" } 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" }
wheels = [ wheels = [
{ 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/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/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]]
@@ -1285,6 +1348,36 @@ 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"
@@ -1571,8 +1664,10 @@ 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" },
@@ -1613,8 +1708,10 @@ 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" }]
@@ -1693,6 +1790,30 @@ 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"