From 9b457278f93c6908483bbf953163e78e955b54b2 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 24 Dec 2025 15:26:41 +1100 Subject: [PATCH 01/17] Add rpm --- Dockerfile.rpmbuild | 108 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 19 ++++++++ poetry.lock | 2 +- pyproject.toml | 4 +- release.sh | 24 ++++++++++ rpm/bouquin.spec | 86 +++++++++++++++++++++++++++++++++++ 6 files changed, 240 insertions(+), 3 deletions(-) create mode 100644 Dockerfile.rpmbuild create mode 100644 rpm/bouquin.spec diff --git a/Dockerfile.rpmbuild b/Dockerfile.rpmbuild new file mode 100644 index 0000000..01201c1 --- /dev/null +++ b/Dockerfile.rpmbuild @@ -0,0 +1,108 @@ +# syntax=docker/dockerfile:1 +FROM fedora:42 + +# rpmbuild in a container does not auto-install BuildRequires. Since we're +# building directly in Docker (not mock), we pre-install the common deps that +# Fedora's pyproject macros will require for Bouquin. +# +# NOTE: bouquin also needs python3dist(sqlcipher4) at build time (because +# %pyproject_buildrequires includes runtime deps). That one is NOT in Fedora; +# we install it from /deps. +RUN set -eux; \ + dnf -y update; \ + dnf -y install \ + rpm-build rpmdevtools \ + redhat-rpm-config \ + gcc \ + make \ + findutils \ + tar \ + gzip \ + rsync \ + python3 \ + python3-devel \ + python3-pip \ + python3-setuptools \ + python3-wheel \ + pyproject-rpm-macros \ + python3-rpm-macros \ + python3-poetry-core \ + desktop-file-utils \ + python3-requests \ + python3-markdown \ + python3-pyside6 \ + xcb-util-cursor ; \ + dnf -y clean all + +RUN set -eux; cat > /usr/local/bin/build-rpm <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +SRC="${SRC:-/src}" +WORKROOT="${WORKROOT:-/work}" +OUT="${OUT:-/out}" +DEPS_DIR="${DEPS_DIR:-/deps}" + +# Install bouquin-sqlcipher4 from local rpm +# Filter out .src.rpm and debug* subpackages if present. +if [ -d "${DEPS_DIR}" ] && compgen -G "${DEPS_DIR}/*.rpm" > /dev/null; then + mapfile -t rpms < <(ls -1 "${DEPS_DIR}"/*.rpm | grep -vE '(\.src\.rpm$|-(debuginfo|debugsource)-)') + if [ "${#rpms[@]}" -gt 0 ]; then + echo "Installing dependency RPMs from ${DEPS_DIR}:" + printf ' - %s\n' "${rpms[@]}" + dnf -y install "${rpms[@]}" + dnf -y clean all + else + echo "NOTE: Only src/debug RPMs found in ${DEPS_DIR}; nothing installed." >&2 + fi +else + echo "NOTE: No RPMs found in ${DEPS_DIR}. If the build fails with missing python3dist(sqlcipher4)," >&2 + echo " mount your bouquin-sqlcipher4 RPM directory as -v :/deps" >&2 +fi + +mkdir -p "${WORKROOT}" "${OUT}" +WORK="${WORKROOT}/src" +rm -rf "${WORK}" +mkdir -p "${WORK}" + +rsync -a --delete \ + --exclude '.git' \ + --exclude '.venv' \ + --exclude 'dist' \ + --exclude 'build' \ + --exclude '__pycache__' \ + --exclude '.pytest_cache' \ + --exclude '.mypy_cache' \ + "${SRC}/" "${WORK}/" + +cd "${WORK}" + +# Determine version from pyproject.toml unless provided +if [ -n "${VERSION:-}" ]; then + ver="${VERSION}" +else + ver="$(grep -m1 '^version = ' pyproject.toml | sed -E 's/version = "([^"]+)".*/\1/')" +fi + +TOPDIR="${WORKROOT}/rpmbuild" +mkdir -p "${TOPDIR}"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} + +tarball="${TOPDIR}/SOURCES/bouquin-${ver}.tar.gz" +tar -czf "${tarball}" --transform "s#^#bouquin/#" . + +cp -v "rpm/bouquin.spec" "${TOPDIR}/SPECS/bouquin.spec" + +rpmbuild -ba "${TOPDIR}/SPECS/bouquin.spec" \ + --define "_topdir ${TOPDIR}" \ + --define "upstream_version ${ver}" + +shopt -s nullglob +cp -v "${TOPDIR}"/RPMS/*/*.rpm "${OUT}/" || true +cp -v "${TOPDIR}"/SRPMS/*.src.rpm "${OUT}/" || true +echo "Artifacts copied to ${OUT}" +EOF + +RUN chmod +x /usr/local/bin/build-rpm + +WORKDIR /work +ENTRYPOINT ["/usr/local/bin/build-rpm"] diff --git a/README.md b/README.md index 4a52ef4..7d4e55f 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,25 @@ sudo apt update sudo apt install bouquin ``` +### Fedora 42 + +```bash +sudo rpm --import https://mig5.net/static/mig5.asc + +sudo tee /etc/yum.repos.d/mig5.repo > /dev/null << 'EOF' +[mig5] +name=mig5 Repository +baseurl=https://rpm.mig5.net/rpm/$basearch +enabled=1 +gpgcheck=1 +repo_gpgcheck=1 +gpgkey=https://mig5.net/static/mig5.asc +EOF + +sudo dnf upgrade --refresh +sudo dnf install bouquin +``` + ### From PyPi/pip * `pip install bouquin` diff --git a/poetry.lock b/poetry.lock index d2932af..c320489 100644 --- a/poetry.lock +++ b/poetry.lock @@ -640,4 +640,4 @@ zstd = ["backports-zstd (>=1.0.0)"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.14" -content-hash = "0241cd7378c45e79da728a23b89defa18f776ada9af1e60f2a19b0d90f3a2c19" +content-hash = "0ce61476b8c35f864d395ab3a4ee4645a1ad2e16aa800cba5e77e5bfee23d6a6" diff --git a/pyproject.toml b/pyproject.toml index f4592bc..9adca9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,8 +13,8 @@ include = ["bouquin/locales/*.json", "bouquin/keys/mig5.asc", "bouquin/fonts/Not python = ">=3.10,<3.14" pyside6 = ">=6.8.1,<7.0.0" bouquin-sqlcipher4 = "^4.12.0" -requests = "^2.32.5" -markdown = "^3.10" +requests = "^2.32.3" +markdown = "^3.7" [tool.poetry.scripts] bouquin = "bouquin.__main__:main" diff --git a/release.sh b/release.sh index 2fab9ac..d358e80 100755 --- a/release.sh +++ b/release.sh @@ -69,4 +69,28 @@ for dist in ${DISTS[@]}; do reprepro -b /home/user/git/repo includedeb "${release}" "${debfile}" done +# RPM +sudo apt-get -y install createrepo-c rpm +docker build -f Dockerfile.rpmbuild -t bouquin-rpm:f42 --progress=plain . +docker run --rm -v "$PWD":/src -v "$PWD/dist/rpm":/out -v "$HOME/git/bouquin-sqlcipher4/dist/rpm":/deps:ro bouquin-rpm:f42 +REPO_ROOT="${HOME}/git/repo_rpm" +RPM_REPO="${REPO_ROOT}/rpm/x86_64" +BUILD_OUTPUT="${HOME}/git/bouquin/dist" +REMOTE="letessier.mig5.net:/opt/repo_rpm" +KEYID="00AE817C24A10C2540461A9C1D7CDE0234DB458D" + +echo "==> Updating RPM repo..." +mkdir -p "$RPM_REPO" +cp "${BUILD_OUTPUT}/rpm/"*.rpm "$RPM_REPO/" + +createrepo_c "$RPM_REPO" + +echo "==> Signing repomd.xml..." +qubes-gpg-client --local-user "$KEYID" --detach-sign --armor "$RPM_REPO/repodata/repomd.xml" > "$RPM_REPO/repodata/repomd.xml.asc" + +echo "==> Syncing repo to server..." +rsync -aHPvz --exclude=.git --delete "$REPO_ROOT/" "$REMOTE/" + +echo "Done!" + ssh wolverine.mig5.net "echo ${VERSION} | tee /opt/www/mig5.net/bouquin/version.txt" diff --git a/rpm/bouquin.spec b/rpm/bouquin.spec new file mode 100644 index 0000000..341d80d --- /dev/null +++ b/rpm/bouquin.spec @@ -0,0 +1,86 @@ +# bouquin Fedora 42 RPM spec using Fedora's pyproject RPM macros (Poetry backend). +# +# NOTE: Bouquin depends on "bouquin-sqlcipher4" project, but the RPM actually +# provides the Python distribution/module as "sqlcipher4". To keep Fedora's +# auto-generated python3dist() Requires correct, we rewrite the dependency key in +# pyproject.toml at build time. +%global upstream_version 0.8.0 + +Name: bouquin +Version: %{upstream_version} +Release: 1%{?dist}.bouquin1 +Summary: A simple, opinionated notebook application (Python/Qt/SQLCipher) + +License: GPL-3.0-or-later +URL: https://git.mig5.net/mig5/bouquin +Source0: %{name}-%{version}.tar.gz + +BuildArch: noarch + +BuildRequires: pyproject-rpm-macros +BuildRequires: python3-devel +BuildRequires: python3-poetry-core +BuildRequires: desktop-file-utils + +# Non-Python runtime dep (Fedora equivalent of Debian's libxcb-cursor0) +Requires: xcb-util-cursor + +# Make sure private repo dependency is pulled in by package name as well. +Requires: python3-sqlcipher4 >= 4.12.0 + +%description +Bouquin is a simple, opinionated notebook application written in Python and Qt, +storing data using SQLCipher. + +%prep +%autosetup -n bouquin + +# Patch dependency name so Fedora's python dependency generator targets the +# provider from bouquin-sqlcipher4 RPM (python3dist(sqlcipher4)). +%{python3} - <<'PY' +from pathlib import Path +import re + +p = Path("pyproject.toml") +txt = p.read_text(encoding="utf-8") + +pattern = re.compile(r'(?ms)(^\[tool\.poetry\.dependencies\]\n.*?)(^\[|\Z)') +m = pattern.search(txt) +if not m: + raise SystemExit("Could not locate [tool.poetry.dependencies] in pyproject.toml") + +deps_block = m.group(1) +deps_block2 = re.sub(r'(?m)^bouquin-sqlcipher4\s*=\s*(".*?")\s*$', r'sqlcipher4 = \1', deps_block) +if deps_block == deps_block2: + raise SystemExit("Did not find bouquin-sqlcipher4 dependency to rewrite") + +p.write_text(txt[:m.start(1)] + deps_block2 + txt[m.end(1):], encoding="utf-8") +PY + +desktop-file-validate debian/bouquin.desktop + +%generate_buildrequires +%pyproject_buildrequires + +%build +%pyproject_wheel + +%install +%pyproject_install +%pyproject_save_files bouquin + +# Desktop integration (mirrors debian/bouquin.install) +install -Dpm 0644 debian/bouquin.desktop %{buildroot}%{_datadir}/applications/bouquin.desktop +install -Dpm 0644 bouquin/icons/bouquin.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/bouquin.svg + +%files -f %{pyproject_files} +%license LICENSE +%doc README.md CHANGELOG.md +%{_bindir}/bouquin + +%{_datadir}/applications/bouquin.desktop +%{_datadir}/icons/hicolor/scalable/apps/bouquin.svg + +%changelog +* Wed Dec 24 2025 Miguel Jacq - %{version}-%{release} +- Initial RPM packaging for Fedora 42 From 26c136900e3a70350e202b1f3a51b1ed1235a67f Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 24 Dec 2025 17:58:20 +1100 Subject: [PATCH 02/17] Add chown and rpmsign step to rpm build --- release.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/release.sh b/release.sh index d358e80..00e552a 100755 --- a/release.sh +++ b/release.sh @@ -73,6 +73,8 @@ done sudo apt-get -y install createrepo-c rpm docker build -f Dockerfile.rpmbuild -t bouquin-rpm:f42 --progress=plain . docker run --rm -v "$PWD":/src -v "$PWD/dist/rpm":/out -v "$HOME/git/bouquin-sqlcipher4/dist/rpm":/deps:ro bouquin-rpm:f42 +sudo chown -R "${USER}" "$PWD/dist" + REPO_ROOT="${HOME}/git/repo_rpm" RPM_REPO="${REPO_ROOT}/rpm/x86_64" BUILD_OUTPUT="${HOME}/git/bouquin/dist" @@ -88,6 +90,10 @@ createrepo_c "$RPM_REPO" echo "==> Signing repomd.xml..." qubes-gpg-client --local-user "$KEYID" --detach-sign --armor "$RPM_REPO/repodata/repomd.xml" > "$RPM_REPO/repodata/repomd.xml.asc" +for file in `ls -1 "$PWD/dist/rpm"`; do + rpmsign --addsign "$PWD/dist/rpm/$file" +done + echo "==> Syncing repo to server..." rsync -aHPvz --exclude=.git --delete "$REPO_ROOT/" "$REMOTE/" From 48e18e04085d17a5d0742b265a0d2546fa753d8b Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 24 Dec 2025 18:27:24 +1100 Subject: [PATCH 03/17] Sign rpms before createrepo_c --- release.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/release.sh b/release.sh index 00e552a..47fedb0 100755 --- a/release.sh +++ b/release.sh @@ -85,15 +85,15 @@ echo "==> Updating RPM repo..." mkdir -p "$RPM_REPO" cp "${BUILD_OUTPUT}/rpm/"*.rpm "$RPM_REPO/" +for file in `ls -1 "$PWD/dist/rpm"`; do + rpmsign --addsign "$PWD/dist/rpm/$file" +done + createrepo_c "$RPM_REPO" echo "==> Signing repomd.xml..." qubes-gpg-client --local-user "$KEYID" --detach-sign --armor "$RPM_REPO/repodata/repomd.xml" > "$RPM_REPO/repodata/repomd.xml.asc" -for file in `ls -1 "$PWD/dist/rpm"`; do - rpmsign --addsign "$PWD/dist/rpm/$file" -done - echo "==> Syncing repo to server..." rsync -aHPvz --exclude=.git --delete "$REPO_ROOT/" "$REMOTE/" From 2eba0df85ade39319fd2d8c0baccc78a58f4db64 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 24 Dec 2025 18:55:55 +1100 Subject: [PATCH 04/17] copy the rpm after signing it, you idiot --- release.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/release.sh b/release.sh index 47fedb0..6165069 100755 --- a/release.sh +++ b/release.sh @@ -83,12 +83,13 @@ KEYID="00AE817C24A10C2540461A9C1D7CDE0234DB458D" echo "==> Updating RPM repo..." mkdir -p "$RPM_REPO" -cp "${BUILD_OUTPUT}/rpm/"*.rpm "$RPM_REPO/" -for file in `ls -1 "$PWD/dist/rpm"`; do - rpmsign --addsign "$PWD/dist/rpm/$file" +for file in `ls -1 "${BUILD_OUTPUT}/rpm"`; do + rpmsign --addsign "${BUILD_OUTPUT}/rpm/$file" done +cp "${BUILD_OUTPUT}/rpm/"*.rpm "$RPM_REPO/" + createrepo_c "$RPM_REPO" echo "==> Signing repomd.xml..." From 9c7cb7ba2bb02fda220c4a52718ee349b05080a4 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 26 Dec 2025 09:03:20 +1100 Subject: [PATCH 05/17] Fix bold/italic/strikethrough styling in certain conditions when toolbar action is used. --- CHANGELOG.md | 4 + bouquin/main_window.py | 59 ++-- bouquin/markdown_editor.py | 579 +++++++++++++++++++++++++++++++++++-- bouquin/toolbar.py | 15 +- 4 files changed, 592 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d94ebeb..259d9ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.8.1 + + * Fix bold/italic/strikethrough styling in certain conditions when toolbar action is used. + # 0.8.0 * Add .desktop file for Debian diff --git a/bouquin/main_window.py b/bouquin/main_window.py index 9b812b4..0cebf24 100644 --- a/bouquin/main_window.py +++ b/bouquin/main_window.py @@ -28,7 +28,6 @@ from PySide6.QtGui import ( QGuiApplication, QKeySequence, QTextCursor, - QTextListFormat, ) from PySide6.QtWidgets import ( QApplication, @@ -1241,46 +1240,58 @@ class MainWindow(QMainWindow): self._toolbar_bound = True def _sync_toolbar(self): - fmt = self.editor.currentCharFormat() + """ + Keep the toolbar "sticky" by reflecting the markdown state at the current caret/selection. + """ c = self.editor.textCursor() + line = c.block().text() + + # Inline styles (markdown-aware) + bold_on = bool(getattr(self.editor, "is_markdown_bold_active", lambda: False)()) + italic_on = bool( + getattr(self.editor, "is_markdown_italic_active", lambda: False)() + ) + strike_on = bool( + getattr(self.editor, "is_markdown_strike_active", lambda: False)() + ) # Block signals so setChecked() doesn't re-trigger actions QSignalBlocker(self.toolBar.actBold) QSignalBlocker(self.toolBar.actItalic) QSignalBlocker(self.toolBar.actStrike) - self.toolBar.actBold.setChecked(fmt.fontWeight() == QFont.Weight.Bold) - self.toolBar.actItalic.setChecked(fmt.fontItalic()) - self.toolBar.actStrike.setChecked(fmt.fontStrikeOut()) + self.toolBar.actBold.setChecked(bold_on) + self.toolBar.actItalic.setChecked(italic_on) + self.toolBar.actStrike.setChecked(strike_on) - # Headings: decide which to check by current point size - def _approx(a, b, eps=0.5): # small float tolerance - return abs(float(a) - float(b)) <= eps - - cur_size = fmt.fontPointSize() or self.editor.font().pointSizeF() - - bH1 = _approx(cur_size, 24) - bH2 = _approx(cur_size, 18) - bH3 = _approx(cur_size, 14) + # Headings: infer from leading markdown markers + heading_level = 0 + m = re.match(r"^\s*(#{1,3})\s+", line) + if m: + heading_level = len(m.group(1)) QSignalBlocker(self.toolBar.actH1) QSignalBlocker(self.toolBar.actH2) QSignalBlocker(self.toolBar.actH3) QSignalBlocker(self.toolBar.actNormal) - self.toolBar.actH1.setChecked(bH1) - self.toolBar.actH2.setChecked(bH2) - self.toolBar.actH3.setChecked(bH3) - self.toolBar.actNormal.setChecked(not (bH1 or bH2 or bH3)) + self.toolBar.actH1.setChecked(heading_level == 1) + self.toolBar.actH2.setChecked(heading_level == 2) + self.toolBar.actH3.setChecked(heading_level == 3) + self.toolBar.actNormal.setChecked(heading_level == 0) + + # Lists: infer from leading markers on the current line + bullets_on = bool(re.match(r"^\s*(?:•|-|\*)\s+", line)) + numbers_on = bool(re.match(r"^\s*\d+\.\s+", line)) + checkboxes_on = bool(re.match(r"^\s*[☐☑]\s+", line)) - # Lists - lst = c.currentList() - bullets_on = lst and lst.format().style() == QTextListFormat.Style.ListDisc - numbers_on = lst and lst.format().style() == QTextListFormat.Style.ListDecimal QSignalBlocker(self.toolBar.actBullets) QSignalBlocker(self.toolBar.actNumbers) - self.toolBar.actBullets.setChecked(bool(bullets_on)) - self.toolBar.actNumbers.setChecked(bool(numbers_on)) + QSignalBlocker(self.toolBar.actCheckboxes) + + self.toolBar.actBullets.setChecked(bullets_on) + self.toolBar.actNumbers.setChecked(numbers_on) + self.toolBar.actCheckboxes.setChecked(checkboxes_on) def _change_font_size(self, delta: int) -> None: """Change font size for all editor tabs and save the setting.""" diff --git a/bouquin/markdown_editor.py b/bouquin/markdown_editor.py index 849f515..b1a6d66 100644 --- a/bouquin/markdown_editor.py +++ b/bouquin/markdown_editor.py @@ -46,7 +46,7 @@ class MarkdownEditor(QTextEdit): _COLLAPSE_LABEL_COLLAPSE = "collapse" _COLLAPSE_LABEL_EXPAND = "expand" _COLLAPSE_END_MARKER = "" - # Accept either "collapse" or "expand" in the header text (older files used only "collapse") + # Accept either "collapse" or "expand" in the header text _COLLAPSE_HEADER_RE = re.compile(r"^([ \t]*)([▸▾])\s+(?:collapse|expand)\s*$") _COLLAPSE_END_RE = re.compile(r"^([ \t]*)\s*$") @@ -114,6 +114,9 @@ class MarkdownEditor(QTextEdit): # Track if we're currently updating text programmatically self._updating = False + # Track pending inline marker insertion (e.g. Italic with no selection) + self._pending_inline_marker: str | None = None + # Help avoid double-click selecting of checkbox self._suppress_next_checkbox_double_click = False @@ -928,6 +931,69 @@ class MarkdownEditor(QTextEdit): return None + def _maybe_skip_over_marker_run(self, key: Qt.Key) -> bool: + """Skip over common markdown marker runs when navigating with Left/Right. + + This prevents the caret from landing *inside* runs like '**', '***', '__', '___' or '~~', + which can cause temporary toolbar-state flicker and makes navigation feel like it takes + "two presses" to get past closing markers. + + Hold any modifier key (Shift/Ctrl/Alt/Meta) to disable this behavior. + """ + c = self.textCursor() + if c.hasSelection(): + return False + + p = c.position() + doc_max = self._doc_max_pos() + + # Right: run starts at the caret + if key == Qt.Key.Key_Right: + if p >= doc_max: + return False + ch = self._text_range(p, p + 1) + if ch not in ("*", "_", "~"): + return False + + run = 0 + while p + run < doc_max and self._text_range(p + run, p + run + 1) == ch: + run += 1 + + # Only skip multi-char runs (bold/strong/emphasis runs or strike) + if ch in ("*", "_") and run >= 2: + c.movePosition(QTextCursor.Right, QTextCursor.MoveAnchor, run) + self.setTextCursor(c) + return True + if ch == "~" and run == 2: + c.movePosition(QTextCursor.Right, QTextCursor.MoveAnchor, 2) + self.setTextCursor(c) + return True + + return False + + # Left: run ends at the caret + if key == Qt.Key.Key_Left: + if p <= 0: + return False + ch = self._text_range(p - 1, p) + if ch not in ("*", "_", "~"): + return False + + run = 0 + while p - 1 - run >= 0 and self._text_range(p - 1 - run, p - run) == ch: + run += 1 + + if ch in ("*", "_") and run >= 2: + c.movePosition(QTextCursor.Left, QTextCursor.MoveAnchor, run) + self.setTextCursor(c) + return True + if ch == "~" and run == 2: + c.movePosition(QTextCursor.Left, QTextCursor.MoveAnchor, 2) + self.setTextCursor(c) + return True + + return False + def keyPressEvent(self, event): """Handle special key events for markdown editing.""" c = self.textCursor() @@ -936,7 +1002,6 @@ class MarkdownEditor(QTextEdit): in_code = self._is_inside_code_block(block) is_fence_line = block.text().strip().startswith("```") - # --- NEW: 3rd backtick shortcut → open code block dialog --- # Only when we're *not* already in a code block or on a fence line. if event.text() == "`" and not (in_code or is_fence_line): line = block.text() @@ -1002,6 +1067,14 @@ class MarkdownEditor(QTextEdit): super().keyPressEvent(event) return + if ( + event.key() in (Qt.Key.Key_Left, Qt.Key.Key_Right) + and event.modifiers() == Qt.KeyboardModifier.NoModifier + and not self.textCursor().hasSelection() + ): + if self._maybe_skip_over_marker_run(event.key()): + return + # --- Step out of a code block with Down at EOF --- if event.key() == Qt.Key.Key_Down: c = self.textCursor() @@ -1509,19 +1582,389 @@ class MarkdownEditor(QTextEdit): # ------------------------ Toolbar action handlers ------------------------ + # ------------------------ Inline markdown helpers ------------------------ + + def _doc_max_pos(self) -> int: + # QTextDocument includes a trailing null character; cursor positions stop before it. + doc = self.document() + return max(0, doc.characterCount() - 1) + + def _text_range(self, start: int, end: int) -> str: + """Return document text between [start, end) using QTextCursor indexing.""" + doc_max = self._doc_max_pos() + start = max(0, min(start, doc_max)) + end = max(0, min(end, doc_max)) + if end < start: + start, end = end, start + tc = QTextCursor(self.document()) + tc.setPosition(start) + tc.setPosition(end, QTextCursor.KeepAnchor) + return tc.selectedText() + + def _selection_wrapped_by( + self, + markers: tuple[str, ...], + *, + require_singletons: bool = False, + ) -> str | None: + """ + If the current selection is wrapped by any marker in `markers`, return the marker. + + Supports both cases: + 1) the selection itself includes the markers, e.g. "**bold**" + 2) the selection is the inner text, with markers immediately adjacent in the doc. + """ + c = self.textCursor() + if not c.hasSelection(): + return None + + sel = c.selectedText() + start = c.selectionStart() + end = c.selectionEnd() + doc_max = self._doc_max_pos() + + # Case 1: selection includes markers + for m in markers: + lm = len(m) + if len(sel) >= 2 * lm and sel.startswith(m) and sel.endswith(m): + return m + + # Case 2: markers adjacent to selection + for m in markers: + lm = len(m) + if start < lm or end + lm > doc_max: + continue + before = self._text_range(start - lm, start) + after = self._text_range(end, end + lm) + if before != m or after != m: + continue + + if require_singletons and lm == 1: + # Ensure the single marker isn't part of a double/triple (e.g. "**" or "__") + ch = m + left_marker_pos = start - 1 + right_marker_pos = end + + if ( + left_marker_pos - 1 >= 0 + and self._text_range(left_marker_pos - 1, left_marker_pos) == ch + ): + continue + if ( + right_marker_pos + 1 <= doc_max + and self._text_range(right_marker_pos + 1, right_marker_pos + 2) + == ch + ): + continue + + return m + + return None + + def _caret_between_markers( + self, marker: str, *, require_singletons: bool = False + ) -> bool: + """True if the caret is exactly between an opening and closing marker (e.g. **|**).""" + c = self.textCursor() + if c.hasSelection(): + return False + + p = c.position() + lm = len(marker) + doc_max = self._doc_max_pos() + if p < lm or p + lm > doc_max: + return False + + before = self._text_range(p - lm, p) + after = self._text_range(p, p + lm) + if before != marker or after != marker: + return False + + if require_singletons and lm == 1: + # Disallow if either side is adjacent to the same char (part of "**", "__", "***", etc.) + ch = marker + if p - 2 >= 0 and self._text_range(p - 2, p - 1) == ch: + return False + if p + 1 <= doc_max and self._text_range(p + 1, p + 2) == ch: + return False + + return True + + def _caret_before_marker( + self, marker: str, *, require_singletons: bool = False + ) -> bool: + """True if the caret is immediately before `marker` (e.g. |**).""" + c = self.textCursor() + if c.hasSelection(): + return False + + p = c.position() + lm = len(marker) + doc_max = self._doc_max_pos() + if p + lm > doc_max: + return False + + after = self._text_range(p, p + lm) + if after != marker: + return False + + if require_singletons and lm == 1: + # Disallow if it's part of a run like "**" or "___". + ch = marker + if p - 1 >= 0 and self._text_range(p - 1, p) == ch: + return False + if p + 1 <= doc_max and self._text_range(p + 1, p + 2) == ch: + return False + + return True + + def _unwrap_selection( + self, marker: str, *, replacement_marker: str | None = None + ) -> bool: + """ + Remove `marker` wrapping from the selection. + If replacement_marker is provided, replace marker with that (e.g. ***text*** -> *text*). + """ + c = self.textCursor() + if not c.hasSelection(): + return False + + sel = c.selectedText() + start = c.selectionStart() + end = c.selectionEnd() + lm = len(marker) + doc_max = self._doc_max_pos() + + def _select_inner( + edit_cursor: QTextCursor, inner_start: int, inner_len: int + ) -> None: + edit_cursor.setPosition(inner_start) + edit_cursor.setPosition(inner_start + inner_len, QTextCursor.KeepAnchor) + self.setTextCursor(edit_cursor) + + # Case 1: selection includes markers + if len(sel) >= 2 * lm and sel.startswith(marker) and sel.endswith(marker): + inner = sel[lm:-lm] + new_text = ( + f"{replacement_marker}{inner}{replacement_marker}" + if replacement_marker is not None + else inner + ) + c.beginEditBlock() + c.insertText(new_text) + c.endEditBlock() + + # Re-select the inner content (not the markers) + inner_start = c.position() - len(new_text) + if replacement_marker is not None: + inner_start += len(replacement_marker) + _select_inner(c, inner_start, len(inner)) + return True + + # Case 2: marker is adjacent to selection + if start >= lm and end + lm <= doc_max: + before = self._text_range(start - lm, start) + after = self._text_range(end, end + lm) + if before == marker and after == marker: + new_text = ( + f"{replacement_marker}{sel}{replacement_marker}" + if replacement_marker is not None + else sel + ) + edit = QTextCursor(self.document()) + edit.beginEditBlock() + edit.setPosition(start - lm) + edit.setPosition(end + lm, QTextCursor.KeepAnchor) + edit.insertText(new_text) + edit.endEditBlock() + + inner_start = (start - lm) + ( + len(replacement_marker) if replacement_marker else 0 + ) + _select_inner(edit, inner_start, len(sel)) + return True + + return False + + def _wrap_selection(self, marker: str) -> None: + """Wrap the current selection with `marker` and keep the content selected.""" + c = self.textCursor() + if not c.hasSelection(): + return + sel = c.selectedText() + start = c.selectionStart() + lm = len(marker) + + c.beginEditBlock() + c.insertText(f"{marker}{sel}{marker}") + c.endEditBlock() + + # Re-select the original content + edit = QTextCursor(self.document()) + edit.setPosition(start + lm) + edit.setPosition(start + lm + len(sel), QTextCursor.KeepAnchor) + self.setTextCursor(edit) + + def _pos_inside_inline_span( + self, + patterns: list[tuple[re.Pattern, int]], + start_in_block: int, + end_in_block: int, + ) -> bool: + """True if [start_in_block, end_in_block] lies within the content region of any pattern match.""" + block_text = self.textCursor().block().text() + for pat, mlen in patterns: + for m in pat.finditer(block_text): + s, e = m.span() + cs, ce = s + mlen, e - mlen + if cs <= start_in_block and end_in_block <= ce: + return True + return False + + def is_markdown_bold_active(self) -> bool: + c = self.textCursor() + bold_markers = ("***", "___", "**", "__") + + if c.hasSelection(): + if self._selection_wrapped_by(bold_markers) is not None: + return True + block = c.block() + start_in_block = c.selectionStart() - block.position() + end_in_block = c.selectionEnd() - block.position() + patterns = [ + (re.compile(r"(? bool: + c = self.textCursor() + italic_markers = ("*", "_", "***", "___") + + if c.hasSelection(): + if ( + self._selection_wrapped_by(italic_markers, require_singletons=True) + is not None + ): + return True + block = c.block() + start_in_block = c.selectionStart() - block.position() + end_in_block = c.selectionEnd() - block.position() + patterns = [ + (re.compile(r"(? bool: + c = self.textCursor() + if c.hasSelection(): + if self._selection_wrapped_by(("~~",)) is not None: + return True + block = c.block() + start_in_block = c.selectionStart() - block.position() + end_in_block = c.selectionEnd() - block.position() + patterns = [(re.compile(r"~~(.+?)~~"), 2)] + return self._pos_inside_inline_span(patterns, start_in_block, end_in_block) + + if self._caret_between_markers("~~"): + return True + block = c.block() + pos_in_block = c.position() - block.position() + patterns = [(re.compile(r"~~(.+?)~~"), 2)] + return self._pos_inside_inline_span(patterns, pos_in_block, pos_in_block) + + # ------------------------ Toolbar action handlers ------------------------ + def apply_weight(self): - """Toggle bold formatting.""" + """Toggle bold formatting (markdown ** / __, and *** / ___).""" cursor = self.textCursor() + if cursor.hasSelection(): - selected = cursor.selectedText() - # Check if already bold - if selected.startswith("**") and selected.endswith("**"): - # Remove bold - new_text = selected[2:-2] - else: - # Add bold - new_text = f"**{selected}**" - cursor.insertText(new_text) + # If bold+italic, toggling bold should leave italic: ***text*** -> *text* + m = self._selection_wrapped_by(("***", "___")) + if m is not None: + repl = "*" if m == "***" else "_" + if self._unwrap_selection(m, replacement_marker=repl): + self.setFocus() + return + + # Normal bold: **text** / __text__ + m = self._selection_wrapped_by(("**", "__")) + if m is not None: + if self._unwrap_selection(m): + self.setFocus() + return + + # Not bold: wrap selection with ** + self._wrap_selection("**") + self.setFocus() + return + + # No selection: + # - If we're between an empty pair (**|**), remove them. + # - If we're inside bold and sitting right before the closing marker (**text|**), + # jump the caret *past* the marker (end-bold) instead of inserting more. + # - Otherwise, insert a new empty pair and place the caret between. + if self._caret_between_markers("**") or self._caret_between_markers("__"): + marker = "**" if self._caret_between_markers("**") else "__" + p = cursor.position() + lm = len(marker) + edit = QTextCursor(self.document()) + edit.beginEditBlock() + edit.setPosition(p - lm) + edit.setPosition(p + lm, QTextCursor.KeepAnchor) + edit.insertText("") + edit.endEditBlock() + edit.setPosition(p - lm) + self.setTextCursor(edit) + elif self.is_markdown_bold_active() and ( + self._caret_before_marker("**") or self._caret_before_marker("__") + ): + marker = "**" if self._caret_before_marker("**") else "__" + cursor.movePosition( + QTextCursor.MoveOperation.Right, + QTextCursor.MoveMode.MoveAnchor, + len(marker), + ) + self.setTextCursor(cursor) + self._pending_inline_marker = None else: # No selection - just insert markers cursor.insertText("****") @@ -1529,44 +1972,120 @@ class MarkdownEditor(QTextEdit): QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.MoveAnchor, 2 ) self.setTextCursor(cursor) + self._pending_inline_marker = "*" # Return focus to editor self.setFocus() def apply_italic(self): - """Toggle italic formatting.""" + """Toggle italic formatting (markdown * / _, and *** / ___).""" cursor = self.textCursor() + if cursor.hasSelection(): - selected = cursor.selectedText() - if ( - selected.startswith("*") - and selected.endswith("*") - and not selected.startswith("**") - ): - new_text = selected[1:-1] - else: - new_text = f"*{selected}*" - cursor.insertText(new_text) + # If bold+italic, toggling italic should leave bold: ***text*** -> **text** + m = self._selection_wrapped_by(("***", "___")) + if m is not None: + repl = "**" if m == "***" else "__" + if self._unwrap_selection(m, replacement_marker=repl): + self.setFocus() + return + + m = self._selection_wrapped_by(("*", "_"), require_singletons=True) + if m is not None: + if self._unwrap_selection(m): + self.setFocus() + return + + self._wrap_selection("*") + self.setFocus() + return + + # No selection: + # - If we're between an empty pair (*|*), remove them. + # - If we're inside italic and sitting right before the closing marker (*text|*), + # jump the caret past the marker (end-italic) instead of inserting more. + # - Otherwise, insert a new empty pair and place the caret between. + if self._caret_between_markers( + "*", require_singletons=True + ) or self._caret_between_markers("_", require_singletons=True): + marker = ( + "*" + if self._caret_between_markers("*", require_singletons=True) + else "_" + ) + p = cursor.position() + lm = len(marker) + edit = QTextCursor(self.document()) + edit.beginEditBlock() + edit.setPosition(p - lm) + edit.setPosition(p + lm, QTextCursor.KeepAnchor) + edit.insertText("") + edit.endEditBlock() + edit.setPosition(p - lm) + self.setTextCursor(edit) + self._pending_inline_marker = None + elif self.is_markdown_italic_active() and ( + self._caret_before_marker("*", require_singletons=True) + or self._caret_before_marker("_", require_singletons=True) + ): + marker = ( + "*" if self._caret_before_marker("*", require_singletons=True) else "_" + ) + cursor.movePosition( + QTextCursor.MoveOperation.Right, + QTextCursor.MoveMode.MoveAnchor, + len(marker), + ) + self.setTextCursor(cursor) + self._pending_inline_marker = None else: cursor.insertText("**") cursor.movePosition( QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.MoveAnchor, 1 ) self.setTextCursor(cursor) + self._pending_inline_marker = "*" # Return focus to editor self.setFocus() def apply_strikethrough(self): - """Toggle strikethrough formatting.""" + """Toggle strikethrough formatting (markdown ~~).""" cursor = self.textCursor() + if cursor.hasSelection(): - selected = cursor.selectedText() - if selected.startswith("~~") and selected.endswith("~~"): - new_text = selected[2:-2] - else: - new_text = f"~~{selected}~~" - cursor.insertText(new_text) + m = self._selection_wrapped_by(("~~",)) + if m is not None: + if self._unwrap_selection(m): + self.setFocus() + return + self._wrap_selection("~~") + self.setFocus() + return + + # No selection: + # - If we're between an empty pair (~~|~~), remove them. + # - If we're inside strike and sitting right before the closing marker (~~text|~~), + # jump the caret past the marker (end-strike) instead of inserting more. + # - Otherwise, insert a new empty pair and place the caret between. + if self._caret_between_markers("~~"): + p = cursor.position() + edit = QTextCursor(self.document()) + edit.beginEditBlock() + edit.setPosition(p - 2) + edit.setPosition(p + 2, QTextCursor.KeepAnchor) + edit.insertText("") + edit.endEditBlock() + edit.setPosition(p - 2) + self.setTextCursor(edit) + elif self.is_markdown_strike_active() and self._caret_before_marker("~~"): + cursor.movePosition( + QTextCursor.MoveOperation.Right, + QTextCursor.MoveMode.MoveAnchor, + 2, + ) + self.setTextCursor(cursor) + self._pending_inline_marker = None else: cursor.insertText("~~~~") cursor.movePosition( diff --git a/bouquin/toolbar.py b/bouquin/toolbar.py index 92383e6..8e8c4bf 100644 --- a/bouquin/toolbar.py +++ b/bouquin/toolbar.py @@ -98,6 +98,7 @@ class ToolBar(QToolBar): self.actNumbers.triggered.connect(self.numbersRequested) self.actCheckboxes = QAction("☑", self) self.actCheckboxes.setToolTip(strings._("toolbar_toggle_checkboxes")) + self.actCheckboxes.setCheckable(True) self.actCheckboxes.triggered.connect(self.checkboxesRequested) # Images @@ -126,22 +127,14 @@ class ToolBar(QToolBar): self.actDocuments = QAction("📁", self) self.actDocuments.setToolTip(strings._("toolbar_documents")) self.actDocuments.triggered.connect(self.documentsRequested) - - # Set exclusive buttons in QActionGroups + # Headings are mutually exclusive (like radio buttons) self.grpHeadings = QActionGroup(self) self.grpHeadings.setExclusive(True) - for a in ( - self.actBold, - self.actItalic, - self.actStrike, - self.actH1, - self.actH2, - self.actH3, - self.actNormal, - ): + for a in (self.actH1, self.actH2, self.actH3, self.actNormal): a.setCheckable(True) a.setActionGroup(self.grpHeadings) + # List types are mutually exclusive self.grpLists = QActionGroup(self) self.grpLists.setExclusive(True) for a in (self.actBullets, self.actNumbers, self.actCheckboxes): From 7e47cef602ff592227f5aa634a79ea1f88bac6c2 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 26 Dec 2025 17:06:45 +1100 Subject: [PATCH 06/17] Move a code block or collapsed section (or generally, anything after a checkbox line until the next checkbox line) when moving an unchecked checkbox line to another day. --- CHANGELOG.md | 1 + bouquin/main_window.py | 213 +++++++++++++++++++++++++++------- tests/test_markdown_editor.py | 10 +- 3 files changed, 179 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 259d9ca..27bd1a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # 0.8.1 * Fix bold/italic/strikethrough styling in certain conditions when toolbar action is used. + * Move a code block or collapsed section (or generally, anything after a checkbox line until the next checkbox line) when moving an unchecked checkbox line to another day. # 0.8.0 diff --git a/bouquin/main_window.py b/bouquin/main_window.py index 0cebf24..2d08863 100644 --- a/bouquin/main_window.py +++ b/bouquin/main_window.py @@ -870,6 +870,11 @@ class MainWindow(QMainWindow): into the rollover target date (today, or next Monday if today is a weekend). + In addition to moving the unchecked checkbox *line* itself, this also + moves any subsequent lines that belong to that unchecked item, up to + (but not including) the next *checked* checkbox line. This allows + code fences, collapsed blocks, and notes under a todo to travel with it. + Returns True if any items were moved, False otherwise. """ if not getattr(self.cfg, "move_todos", False): @@ -884,7 +889,9 @@ class MainWindow(QMainWindow): # Regexes for markdown headings and checkboxes heading_re = re.compile(r"^\s{0,3}(#+)\s+(.*)$") - unchecked_re = re.compile(r"^\s*-\s*\[[\s☐]\]\s+") + unchecked_re = re.compile(r"^(\s*)-\s*\[[\s☐]\]\s+(.*)$") + checked_re = re.compile(r"^(\s*)-\s*\[[xX☑]\]\s+(.*)$") + fence_re = re.compile(r"^\s*(`{3,}|~{3,})") def _normalize_heading(text: str) -> str: """ @@ -895,13 +902,47 @@ class MainWindow(QMainWindow): text = re.sub(r"\s+#+\s*$", "", text) return text.strip() - def _insert_todos_under_heading( + def _update_fence_state( + line: str, in_fence: bool, fence_marker: str | None + ) -> tuple[bool, str | None]: + """ + Track fenced code blocks (``` / ~~~). We ignore checkbox markers inside + fences so we don't accidentally split/move based on "- [x]" that appears + in code. + """ + m = fence_re.match(line) + if not m: + return in_fence, fence_marker + + marker = m.group(1) + if not in_fence: + return True, marker + + # Close only when we see a fence of the same char and >= length + if ( + fence_marker + and marker[0] == fence_marker[0] + and len(marker) >= len(fence_marker) + ): + return False, None + + return in_fence, fence_marker + + def _is_list_item(line: str) -> bool: + s = line.lstrip() + return bool( + re.match(r"^([-*+]\s+|\d+\.\s+)", s) + or unchecked_re.match(line) + or checked_re.match(line) + ) + + def _insert_blocks_under_heading( target_lines: list[str], heading_level: int, heading_text: str, - todos: list[str], + blocks: list[list[str]], ) -> list[str]: - """Ensure a heading exists and append todos to the end of its section.""" + """Ensure a heading exists and append blocks to the end of its section.""" normalized = _normalize_heading(heading_text) # 1) Find existing heading with same text (any level) @@ -941,15 +982,33 @@ class MainWindow(QMainWindow): ): insert_at -= 1 - for todo in todos: - target_lines.insert(insert_at, todo) - insert_at += 1 + # Insert blocks (preserve internal blank lines) + for block in blocks: + if not block: + continue + + # Avoid gluing a paragraph to the new block unless both look like list items + if ( + insert_at > start_idx + 1 + and target_lines[insert_at - 1].strip() != "" + and block[0].strip() != "" + and not ( + _is_list_item(target_lines[insert_at - 1]) + and _is_list_item(block[0]) + ) + ): + target_lines.insert(insert_at, "") + insert_at += 1 + + for line in block: + target_lines.insert(insert_at, line) + insert_at += 1 return target_lines - # Collect moved todos as (heading_info, item_text) + # Collect moved blocks as (heading_info, block_lines) # heading_info is either None or (level, heading_text) - moved_items: list[tuple[tuple[int, str] | None, str]] = [] + moved_blocks: list[tuple[tuple[int, str] | None, list[str]]] = [] any_moved = False # Look back N days (yesterday = 1, up to `days_back`) @@ -965,26 +1024,81 @@ class MainWindow(QMainWindow): moved_from_this_day = False current_heading: tuple[int, str] | None = None - for line in lines: - # Track the last seen heading (# / ## / ###) - m_head = heading_re.match(line) - if m_head: - level = len(m_head.group(1)) - heading_text = _normalize_heading(m_head.group(2)) - if level <= 3: - current_heading = (level, heading_text) - # Keep headings in the original day - remaining_lines.append(line) - continue + in_fence = False + fence_marker: str | None = None - # Unchecked markdown checkboxes: "- [ ] " or "- [☐] " - if unchecked_re.match(line): - item_text = unchecked_re.sub("", line) - moved_items.append((current_heading, item_text)) - moved_from_this_day = True - any_moved = True - else: - remaining_lines.append(line) + i = 0 + while i < len(lines): + line = lines[i] + + # If we're not in a fenced code block, we can interpret headings/checkboxes + if not in_fence: + # Track the last seen heading (# / ## / ###) + m_head = heading_re.match(line) + if m_head: + level = len(m_head.group(1)) + heading_text = _normalize_heading(m_head.group(2)) + if level <= 3: + current_heading = (level, heading_text) + # Keep headings in the original day (only headings ABOVE a moved block are "carried") + remaining_lines.append(line) + in_fence, fence_marker = _update_fence_state( + line, in_fence, fence_marker + ) + i += 1 + continue + + # Start of an unchecked checkbox block + m_unchecked = unchecked_re.match(line) + if m_unchecked: + indent = m_unchecked.group(1) or "" + item_text = m_unchecked.group(2) + block: list[str] = [f"{indent}- [ ] {item_text}"] + + i += 1 + # Consume subsequent lines until the next *checked* checkbox + # (ignoring any "- [x]" that appear inside fenced code blocks) + block_in_fence = in_fence + block_fence_marker = fence_marker + + while i < len(lines): + nxt = lines[i] + + # If we're not inside a fence, a checked checkbox ends the block + if not block_in_fence and checked_re.match(nxt): + break + + # Normalize any unchecked checkbox lines inside the block + m_inner_unchecked = ( + unchecked_re.match(nxt) if not block_in_fence else None + ) + if m_inner_unchecked: + inner_indent = m_inner_unchecked.group(1) or "" + inner_text = m_inner_unchecked.group(2) + block.append(f"{inner_indent}- [ ] {inner_text}") + else: + block.append(nxt) + + # Update fence state after consuming the line + block_in_fence, block_fence_marker = _update_fence_state( + nxt, block_in_fence, block_fence_marker + ) + i += 1 + + # Carry the last heading *above* the unchecked checkbox + moved_blocks.append((current_heading, block)) + moved_from_this_day = True + any_moved = True + + # We consumed the block; keep scanning from the checked checkbox (or EOF) + continue + + # Default: keep the line on the original day + remaining_lines.append(line) + in_fence, fence_marker = _update_fence_state( + line, in_fence, fence_marker + ) + i += 1 if moved_from_this_day: modified_text = "\n".join(remaining_lines) @@ -998,33 +1112,46 @@ class MainWindow(QMainWindow): if not any_moved: return False - # --- Merge all moved items into the *target* date --- + # --- Merge all moved blocks into the *target* date --- target_text = self.db.get_entry(target_iso) or "" target_lines = target_text.split("\n") if target_text else [] - by_heading: dict[tuple[int, str], list[str]] = {} - plain_items: list[str] = [] + by_heading: dict[tuple[int, str], list[list[str]]] = {} + plain_blocks: list[list[str]] = [] - for heading_info, item_text in moved_items: - todo_line = f"- [ ] {item_text}" + for heading_info, block in moved_blocks: if heading_info is None: - # No heading above this checkbox in the source: behave as before - plain_items.append(todo_line) + plain_blocks.append(block) else: - by_heading.setdefault(heading_info, []).append(todo_line) + by_heading.setdefault(heading_info, []).append(block) - # First insert all items that have headings - for (level, heading_text), todos in by_heading.items(): - target_lines = _insert_todos_under_heading( - target_lines, level, heading_text, todos + # First insert all blocks that have headings + for (level, heading_text), blocks in by_heading.items(): + target_lines = _insert_blocks_under_heading( + target_lines, level, heading_text, blocks ) - # Then append all items without headings at the end, like before - if plain_items: + # Then append all blocks without headings at the end, like before + if plain_blocks: if target_lines and target_lines[-1].strip(): target_lines.append("") # one blank line before the "unsectioned" todos - target_lines.extend(plain_items) + first = True + for block in plain_blocks: + if not block: + continue + if ( + not first + and target_lines + and target_lines[-1].strip() != "" + and block[0].strip() != "" + and not ( + _is_list_item(target_lines[-1]) and _is_list_item(block[0]) + ) + ): + target_lines.append("") + target_lines.extend(block) + first = False new_target_text = "\n".join(target_lines) if not new_target_text.endswith("\n"): diff --git a/tests/test_markdown_editor.py b/tests/test_markdown_editor.py index a36a09e..9dac5d6 100644 --- a/tests/test_markdown_editor.py +++ b/tests/test_markdown_editor.py @@ -1,6 +1,7 @@ import base64 import pytest +import re from bouquin.markdown_editor import MarkdownEditor from bouquin.markdown_highlighter import MarkdownHighlighter from bouquin.theme import Theme, ThemeConfig, ThemeManager @@ -72,8 +73,13 @@ def test_apply_styles_and_headings(editor, qtbot): editor.apply_italic() editor.apply_strikethrough() editor.apply_heading(24) - md = editor.to_markdown() - assert "**" in md and "*~~~~*" in md + md = editor.to_markdown().strip() + + assert md.startswith("# ") + assert "~~hello world~~" in md + assert re.search( + r"\*{2,3}~~hello world~~\*{2,3}", md + ) # bold or bold+italic wrapping strike def test_toggle_lists_and_checkboxes(editor): From dce124e083e00ec6b9dc28d482160d3d6638266b Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 26 Dec 2025 18:01:56 +1100 Subject: [PATCH 07/17] prep for 0.8.1 --- debian/changelog | 7 +++++++ rpm/bouquin.spec | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 0216393..036fb4e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +bouquin (0.8.1) unstable; urgency=medium + + * Fix bold/italic/strikethrough styling in certain conditions when toolbar action is used. + * Move a code block or collapsed section (or generally, anything after a checkbox line until the next checkbox line) when moving an unchecked checkbox line to another day. + + -- Miguel Jacq Tue, 26 Dec 2025 18:00:00 +1100 + bouquin (0.8.0) unstable; urgency=medium * Add .desktop file for Debian diff --git a/rpm/bouquin.spec b/rpm/bouquin.spec index 341d80d..f7d3c37 100644 --- a/rpm/bouquin.spec +++ b/rpm/bouquin.spec @@ -4,7 +4,7 @@ # provides the Python distribution/module as "sqlcipher4". To keep Fedora's # auto-generated python3dist() Requires correct, we rewrite the dependency key in # pyproject.toml at build time. -%global upstream_version 0.8.0 +%global upstream_version 0.8.1 Name: bouquin Version: %{upstream_version} @@ -82,5 +82,8 @@ install -Dpm 0644 bouquin/icons/bouquin.svg %{buildroot}%{_datadir}/icons/hicolo %{_datadir}/icons/hicolor/scalable/apps/bouquin.svg %changelog +* Fri Dec 26 2025 Miguel Jacq - %{version}-%{release} +- Fix bold/italic/strikethrough styling in certain conditions when toolbar action is used. +- Move a code block or collapsed section (or generally, anything after a checkbox line until the next checkbox line) when moving an unchecked checkbox line to another day. * Wed Dec 24 2025 Miguel Jacq - %{version}-%{release} - Initial RPM packaging for Fedora 42 From 827565838f9dd03b9994fe60c8e93b9adc0e5ba7 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 26 Dec 2025 18:02:04 +1100 Subject: [PATCH 08/17] Bump to 0.8.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9adca9a..380e67b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "bouquin" -version = "0.8.0" +version = "0.8.1" description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher." authors = ["Miguel Jacq "] readme = "README.md" From 04f67a786f6da93bdb44fc24e88805cca7f5ff8d Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 31 Dec 2025 16:09:16 +1100 Subject: [PATCH 09/17] Add ability to delete an invoice via Manage Invoices dialog --- CHANGELOG.md | 4 +++ bouquin/db.py | 12 ++++++++ bouquin/invoices.py | 66 +++++++++++++++++++++++++++++++++++++++++ bouquin/locales/en.json | 3 +- bouquin/locales/fr.json | 3 +- debian/changelog | 6 ++++ rpm/bouquin.spec | 4 ++- 7 files changed, 95 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27bd1a3..808fb36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.8.2 + + * Add ability to delete an invoice via 'Manage Invoices' dialog + # 0.8.1 * Fix bold/italic/strikethrough styling in certain conditions when toolbar action is used. diff --git a/bouquin/db.py b/bouquin/db.py index d1c6a69..157aae8 100644 --- a/bouquin/db.py +++ b/bouquin/db.py @@ -2392,6 +2392,18 @@ class DBManager: (document_id, invoice_id), ) + def delete_invoice(self, invoice_id: int) -> None: + """Delete an invoice. + + Related invoice line items and invoice ↔ time log links are removed via + ON DELETE CASCADE. + """ + with self.conn: + self.conn.execute( + "DELETE FROM invoices WHERE id = ?", + (invoice_id,), + ) + def time_logs_for_range( self, project_id: int, diff --git a/bouquin/invoices.py b/bouquin/invoices.py index a0b50cb..fde6a92 100644 --- a/bouquin/invoices.py +++ b/bouquin/invoices.py @@ -1065,6 +1065,10 @@ class InvoicesDialog(QDialog): btn_row = QHBoxLayout() btn_row.addStretch(1) + delete_btn = QPushButton(strings._("delete")) + delete_btn.clicked.connect(self._on_delete_clicked) + btn_row.addWidget(delete_btn) + close_btn = QPushButton(strings._("close")) close_btn.clicked.connect(self.accept) btn_row.addWidget(close_btn) @@ -1073,6 +1077,68 @@ class InvoicesDialog(QDialog): self._reload_invoices() + # ----------------------------------------------------------------- deletion + + def _on_delete_clicked(self) -> None: + """Delete the currently selected invoice.""" + row = self.table.currentRow() + if row < 0: + sel = self.table.selectionModel().selectedRows() + if sel: + row = sel[0].row() + if row < 0: + QMessageBox.information( + self, + strings._("delete"), + strings._("invoice_required"), + ) + return + + base_item = self.table.item(row, self.COL_NUMBER) + if base_item is None: + return + + inv_id = base_item.data(Qt.ItemDataRole.UserRole) + if not inv_id: + return + + invoice_number = (base_item.text() or "").strip() or "?" + proj_item = self.table.item(row, self.COL_PROJECT) + project_name = (proj_item.text() if proj_item is not None else "").strip() + + label = strings._("delete") + prompt = ( + f"{label} '{invoice_number}'" + + (f" ({project_name})" if project_name else "") + + "?" + ) + + resp = QMessageBox.question( + self, + label, + prompt, + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No, + ) + if resp != QMessageBox.StandardButton.Yes: + return + + # Remove any automatically created due-date reminder. + if self.cfg.reminders: + self._remove_invoice_due_reminder(row, int(inv_id)) + + try: + self._db.delete_invoice(int(inv_id)) + except Exception as e: + QMessageBox.warning( + self, + strings._("error"), + f"Failed to delete invoice: {e}", + ) + return + + self._reload_invoices() + # ------------------------------------------------------------------ helpers def _reload_projects(self) -> None: diff --git a/bouquin/locales/en.json b/bouquin/locales/en.json index 26a4d5c..f1f86dd 100644 --- a/bouquin/locales/en.json +++ b/bouquin/locales/en.json @@ -436,5 +436,6 @@ "invoice_invalid_date_format": "Invalid date format", "invoice_invalid_tax_rate": "The tax rate is invalid", "invoice_no_items": "There are no items in the invoice", - "invoice_number_required": "An invoice number is required" + "invoice_number_required": "An invoice number is required", + "invoice_required": "Please select a specific invoice before trying to delete an invoice." } diff --git a/bouquin/locales/fr.json b/bouquin/locales/fr.json index d82890d..87a6a16 100644 --- a/bouquin/locales/fr.json +++ b/bouquin/locales/fr.json @@ -432,5 +432,6 @@ "invoice_invalid_date_format": "Format de date invalide", "invoice_invalid_tax_rate": "Le taux de TVA est invalide", "invoice_no_items": "La facture ne contient aucun article", - "invoice_number_required": "Un numéro de facture est requis" + "invoice_number_required": "Un numéro de facture est requis", + "invoice_required": "Veuillez sélectionner une facture spécifique avant d'essayer de supprimer la facture." } diff --git a/debian/changelog b/debian/changelog index 036fb4e..26075f4 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +bouquin (0.8.2) unstable; urgency=medium + + * Add ability to delete an invoice via 'Manage Invoices' dialog + + -- Miguel Jacq Wed, 31 Dec 2025 16:00:00 +1100 + bouquin (0.8.1) unstable; urgency=medium * Fix bold/italic/strikethrough styling in certain conditions when toolbar action is used. diff --git a/rpm/bouquin.spec b/rpm/bouquin.spec index f7d3c37..2f8d442 100644 --- a/rpm/bouquin.spec +++ b/rpm/bouquin.spec @@ -4,7 +4,7 @@ # provides the Python distribution/module as "sqlcipher4". To keep Fedora's # auto-generated python3dist() Requires correct, we rewrite the dependency key in # pyproject.toml at build time. -%global upstream_version 0.8.1 +%global upstream_version 0.8.2 Name: bouquin Version: %{upstream_version} @@ -82,6 +82,8 @@ install -Dpm 0644 bouquin/icons/bouquin.svg %{buildroot}%{_datadir}/icons/hicolo %{_datadir}/icons/hicolor/scalable/apps/bouquin.svg %changelog +* Wed Dec 31 2025 Miguel Jacq - %{version}-%{release} +- Add ability to delete an invoice via 'Manage Invoices' dialog * Fri Dec 26 2025 Miguel Jacq - %{version}-%{release} - Fix bold/italic/strikethrough styling in certain conditions when toolbar action is used. - Move a code block or collapsed section (or generally, anything after a checkbox line until the next checkbox line) when moving an unchecked checkbox line to another day. From f8aab05cb74845569d85b31f4308e18a585b5f07 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 31 Dec 2025 16:09:29 +1100 Subject: [PATCH 10/17] Bump to 0.8.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 380e67b..868fb26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "bouquin" -version = "0.8.1" +version = "0.8.2" description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher." authors = ["Miguel Jacq "] readme = "README.md" From b192264dbf2b3eb7be20f87eec760958b5d160a8 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 31 Dec 2025 17:12:21 +1100 Subject: [PATCH 11/17] server migration --- release.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/release.sh b/release.sh index 6165069..14ecb4e 100755 --- a/release.sh +++ b/release.sh @@ -24,18 +24,18 @@ fi set +e sed -i s/version.*$/version\ =\ \"${VERSION}\"/g pyproject.toml -git add pyproject.toml -git commit -m "Bump to ${VERSION}" -git push origin main +#git add pyproject.toml +#git commit -m "Bump to ${VERSION}" +#git push origin main set -e # Clean caches etc -filedust -y . +#filedust -y . # Publish to Pypi -poetry build -poetry publish +#poetry build +#poetry publish # Make AppImage sudo apt-get -y install libfuse-dev @@ -100,4 +100,4 @@ rsync -aHPvz --exclude=.git --delete "$REPO_ROOT/" "$REMOTE/" echo "Done!" -ssh wolverine.mig5.net "echo ${VERSION} | tee /opt/www/mig5.net/bouquin/version.txt" +ssh lupin.mig5.net "echo ${VERSION} | tee /opt/www/mig5.net/bouquin/version.txt" From 7bb2746a0fcdc67430c4353b045c11a3503da923 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Thu, 1 Jan 2026 17:09:37 +1100 Subject: [PATCH 12/17] Prep for supporting other fedora versions later --- release.sh | 55 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/release.sh b/release.sh index 14ecb4e..5ab4237 100755 --- a/release.sh +++ b/release.sh @@ -24,18 +24,18 @@ fi set +e sed -i s/version.*$/version\ =\ \"${VERSION}\"/g pyproject.toml -#git add pyproject.toml -#git commit -m "Bump to ${VERSION}" -#git push origin main +git add pyproject.toml +git commit -m "Bump to ${VERSION}" +git push origin main set -e # Clean caches etc -#filedust -y . +filedust -y . # Publish to Pypi -#poetry build -#poetry publish +poetry build +poetry publish # Make AppImage sudo apt-get -y install libfuse-dev @@ -70,31 +70,42 @@ for dist in ${DISTS[@]}; do done # RPM -sudo apt-get -y install createrepo-c rpm -docker build -f Dockerfile.rpmbuild -t bouquin-rpm:f42 --progress=plain . -docker run --rm -v "$PWD":/src -v "$PWD/dist/rpm":/out -v "$HOME/git/bouquin-sqlcipher4/dist/rpm":/deps:ro bouquin-rpm:f42 -sudo chown -R "${USER}" "$PWD/dist" - REPO_ROOT="${HOME}/git/repo_rpm" RPM_REPO="${REPO_ROOT}/rpm/x86_64" BUILD_OUTPUT="${HOME}/git/bouquin/dist" REMOTE="letessier.mig5.net:/opt/repo_rpm" KEYID="00AE817C24A10C2540461A9C1D7CDE0234DB458D" - -echo "==> Updating RPM repo..." mkdir -p "$RPM_REPO" -for file in `ls -1 "${BUILD_OUTPUT}/rpm"`; do - rpmsign --addsign "${BUILD_OUTPUT}/rpm/$file" +sudo apt-get -y install createrepo-c rpm + +DISTS=( + fedora:42 +) + +for dist in ${DISTS[@]}; do + release=$(echo ${dist} | cut -d: -f2) + docker build -f Dockerfile.rpmbuild \ + -t bouquin-rpm:${release} \ + --progress=plain \ + --build-arg BASE_IMAGE=${dist} \ + . + + docker run --rm -v "$PWD":/src -v "$PWD/dist/rpm":/out -v "$HOME/git/bouquin-sqlcipher4/dist/rpm":/deps:ro bouquin-rpm:${release} + sudo chown -R "${USER}" "$PWD/dist" + + for file in `ls -1 "${BUILD_OUTPUT}/rpm"`; do + rpmsign --addsign "${BUILD_OUTPUT}/rpm/$file" + done + + cp "${BUILD_OUTPUT}/rpm/"*.rpm "$RPM_REPO/" + + createrepo_c "$RPM_REPO" + + echo "==> Signing repomd.xml..." + qubes-gpg-client --local-user "$KEYID" --detach-sign --armor "$RPM_REPO/repodata/repomd.xml" > "$RPM_REPO/repodata/repomd.xml.asc" done -cp "${BUILD_OUTPUT}/rpm/"*.rpm "$RPM_REPO/" - -createrepo_c "$RPM_REPO" - -echo "==> Signing repomd.xml..." -qubes-gpg-client --local-user "$KEYID" --detach-sign --armor "$RPM_REPO/repodata/repomd.xml" > "$RPM_REPO/repodata/repomd.xml.asc" - echo "==> Syncing repo to server..." rsync -aHPvz --exclude=.git --delete "$REPO_ROOT/" "$REMOTE/" From 5f89c4286efd7487dfeca1270234bce6d5aa748a Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 3 Jan 2026 12:11:49 +1100 Subject: [PATCH 13/17] Fix releasing for Fedora --- Dockerfile.rpmbuild | 4 +++- README.md | 2 +- release.sh | 20 ++++++++++++-------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Dockerfile.rpmbuild b/Dockerfile.rpmbuild index 01201c1..9013b6e 100644 --- a/Dockerfile.rpmbuild +++ b/Dockerfile.rpmbuild @@ -42,11 +42,13 @@ SRC="${SRC:-/src}" WORKROOT="${WORKROOT:-/work}" OUT="${OUT:-/out}" DEPS_DIR="${DEPS_DIR:-/deps}" +VERSION_ID="$(grep VERSION_ID /etc/os-release | cut -d= -f2)" +echo "Version ID is ${VERSION_ID}" # Install bouquin-sqlcipher4 from local rpm # Filter out .src.rpm and debug* subpackages if present. if [ -d "${DEPS_DIR}" ] && compgen -G "${DEPS_DIR}/*.rpm" > /dev/null; then - mapfile -t rpms < <(ls -1 "${DEPS_DIR}"/*.rpm | grep -vE '(\.src\.rpm$|-(debuginfo|debugsource)-)') + mapfile -t rpms < <(ls -1 "${DEPS_DIR}"/*.rpm | grep -vE '(\.src\.rpm$|-(debuginfo|debugsource)-)' | grep "${VERSION_ID}") if [ "${#rpms[@]}" -gt 0 ]; then echo "Installing dependency RPMs from ${DEPS_DIR}:" printf ' - %s\n' "${rpms[@]}" diff --git a/README.md b/README.md index 7d4e55f..c01a35e 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ sudo rpm --import https://mig5.net/static/mig5.asc sudo tee /etc/yum.repos.d/mig5.repo > /dev/null << 'EOF' [mig5] name=mig5 Repository -baseurl=https://rpm.mig5.net/rpm/$basearch +baseurl=https://rpm.mig5.net/rpm/$releasever/$basearch enabled=1 gpgcheck=1 repo_gpgcheck=1 diff --git a/release.sh b/release.sh index 5ab4237..af83196 100755 --- a/release.sh +++ b/release.sh @@ -69,15 +69,13 @@ for dist in ${DISTS[@]}; do reprepro -b /home/user/git/repo includedeb "${release}" "${debfile}" done -# RPM -REPO_ROOT="${HOME}/git/repo_rpm" -RPM_REPO="${REPO_ROOT}/rpm/x86_64" -BUILD_OUTPUT="${HOME}/git/bouquin/dist" -REMOTE="letessier.mig5.net:/opt/repo_rpm" -KEYID="00AE817C24A10C2540461A9C1D7CDE0234DB458D" -mkdir -p "$RPM_REPO" +# RPM sudo apt-get -y install createrepo-c rpm +BUILD_OUTPUT="${HOME}/git/bouquin/dist" +KEYID="00AE817C24A10C2540461A9C1D7CDE0234DB458D" +REPO_ROOT="${HOME}/git/repo_rpm" +REMOTE="letessier.mig5.net:/opt/repo_rpm" DISTS=( fedora:42 @@ -85,7 +83,13 @@ DISTS=( for dist in ${DISTS[@]}; do release=$(echo ${dist} | cut -d: -f2) - docker build -f Dockerfile.rpmbuild \ + REPO_RELEASE_ROOT="${REPO_ROOT}/fc${release}" + RPM_REPO="${REPO_RELEASE_ROOT}/rpm/x86_64" + mkdir -p "$RPM_REPO" + + docker build \ + --no-cache \ + -f Dockerfile.rpmbuild \ -t bouquin-rpm:${release} \ --progress=plain \ --build-arg BASE_IMAGE=${dist} \ From dd1ae74b192ec8783145526b48bd88142fbc5f4f Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 3 Jan 2026 12:50:36 +1100 Subject: [PATCH 14/17] remove 'fc' from release root --- README.md | 2 +- release.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c01a35e..30f7ce1 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ sudo rpm --import https://mig5.net/static/mig5.asc sudo tee /etc/yum.repos.d/mig5.repo > /dev/null << 'EOF' [mig5] name=mig5 Repository -baseurl=https://rpm.mig5.net/rpm/$releasever/$basearch +baseurl=https://rpm.mig5.net/$releasever/rpm/$basearch enabled=1 gpgcheck=1 repo_gpgcheck=1 diff --git a/release.sh b/release.sh index af83196..63e2723 100755 --- a/release.sh +++ b/release.sh @@ -83,7 +83,7 @@ DISTS=( for dist in ${DISTS[@]}; do release=$(echo ${dist} | cut -d: -f2) - REPO_RELEASE_ROOT="${REPO_ROOT}/fc${release}" + REPO_RELEASE_ROOT="${REPO_ROOT}/${release}" RPM_REPO="${REPO_RELEASE_ROOT}/rpm/x86_64" mkdir -p "$RPM_REPO" From 9f399c589d9db4e91a55fb27f103cd11aab7c4d1 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 9 Jan 2026 12:07:46 +1100 Subject: [PATCH 15/17] Update urllib3 dependency to resolve CVE-2026-21441 --- CHANGELOG.md | 4 ++++ poetry.lock | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 808fb36..f5cb7bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.8.3 (unreleased) + + * Update urllib3 dependency to resolve CVE-2026-21441 + # 0.8.2 * Add ability to delete an invoice via 'Manage Invoices' dialog diff --git a/poetry.lock b/poetry.lock index c320489..af05c5f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -622,13 +622,13 @@ files = [ [[package]] name = "urllib3" -version = "2.6.2" +version = "2.6.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" files = [ - {file = "urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd"}, - {file = "urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797"}, + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, ] [package.extras] From 7f2c88f52b3575634e6bb52e7e9ca122225a8fa0 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 30 Jan 2026 16:49:45 +1100 Subject: [PATCH 16/17] Fix carrying over data to next day from over-capturing data belonging to next header section Other dependency updates --- CHANGELOG.md | 4 +- bouquin/main_window.py | 128 +++++++++++++++++- debian/changelog | 8 ++ poetry.lock | 297 +++++++++++++++++++++-------------------- pyproject.toml | 2 +- rpm/bouquin.spec | 6 +- 6 files changed, 290 insertions(+), 155 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5cb7bb..01266cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ -# 0.8.3 (unreleased) +# 0.8.3 * Update urllib3 dependency to resolve CVE-2026-21441 + * Fix carrying over data to next day from over-capturing data belonging to next header section + * Other dependency updates # 0.8.2 diff --git a/bouquin/main_window.py b/bouquin/main_window.py index 2d08863..2759272 100644 --- a/bouquin/main_window.py +++ b/bouquin/main_window.py @@ -871,9 +871,11 @@ class MainWindow(QMainWindow): is a weekend). In addition to moving the unchecked checkbox *line* itself, this also - moves any subsequent lines that belong to that unchecked item, up to - (but not including) the next *checked* checkbox line. This allows - code fences, collapsed blocks, and notes under a todo to travel with it. + moves any subsequent lines that belong to that unchecked item, stopping + at the next *checked* checkbox line **or** the next markdown heading. + + This allows code fences, collapsed blocks, and notes under a todo to + travel with it without accidentally pulling in the next section. Returns True if any items were moved, False otherwise. """ @@ -1006,6 +1008,110 @@ class MainWindow(QMainWindow): return target_lines + def _prune_empty_headings(src_lines: list[str]) -> list[str]: + """Remove markdown headings whose section became empty. + + The rollover logic removes unchecked todo *blocks* but intentionally keeps + headings on the source day so we can re-create the same section on the + target day. If a heading ends up with no remaining content (including + empty subheadings), we should remove it from the source day too. + + Headings inside fenced code blocks are ignored. + """ + + # Identify headings (outside fences) and their levels + heading_levels: dict[int, int] = {} + heading_indices: list[int] = [] + + in_f = False + f_mark: str | None = None + for idx, ln in enumerate(src_lines): + if not in_f: + m = heading_re.match(ln) + if m: + heading_indices.append(idx) + heading_levels[idx] = len(m.group(1)) + in_f, f_mark = _update_fence_state(ln, in_f, f_mark) + + if not heading_indices: + return src_lines + + # Compute each heading's section boundary: next heading with level <= current + boundary: dict[int, int] = {} + stack: list[int] = [] + for idx in heading_indices: + lvl = heading_levels[idx] + while stack and lvl <= heading_levels[stack[-1]]: + boundary[stack.pop()] = idx + stack.append(idx) + for idx in stack: + boundary[idx] = len(src_lines) + + # Build parent/children relationships based on heading levels + children: dict[int, list[int]] = {} + parent_stack: list[int] = [] + for idx in heading_indices: + lvl = heading_levels[idx] + while parent_stack and lvl <= heading_levels[parent_stack[-1]]: + parent_stack.pop() + if parent_stack: + children.setdefault(parent_stack[-1], []).append(idx) + parent_stack.append(idx) + + # Determine whether each heading has any non-heading, non-blank content in its span + has_body: dict[int, bool] = {} + for h_idx in heading_indices: + end = boundary[h_idx] + body = False + in_f = False + f_mark = None + for j in range(h_idx + 1, end): + ln = src_lines[j] + if not in_f: + if ln.strip() and not heading_re.match(ln): + body = True + break + in_f, f_mark = _update_fence_state(ln, in_f, f_mark) + has_body[h_idx] = body + + # Bottom-up: keep headings that have body content or any kept child headings + keep: dict[int, bool] = {} + for h_idx in reversed(heading_indices): + keep_child = any(keep.get(ch, False) for ch in children.get(h_idx, [])) + keep[h_idx] = has_body[h_idx] or keep_child + + remove_set = {idx for idx, k in keep.items() if not k} + if not remove_set: + return src_lines + + # Remove empty headings and any immediate blank lines following them + out: list[str] = [] + i = 0 + while i < len(src_lines): + if i in remove_set: + i += 1 + while i < len(src_lines) and src_lines[i].strip() == "": + i += 1 + continue + out.append(src_lines[i]) + i += 1 + + # Normalize excessive blank lines created by removals + cleaned: list[str] = [] + prev_blank = False + for ln in out: + blank = ln.strip() == "" + if blank and prev_blank: + continue + cleaned.append(ln) + prev_blank = blank + + while cleaned and cleaned[0].strip() == "": + cleaned.pop(0) + while cleaned and cleaned[-1].strip() == "": + cleaned.pop() + return cleaned + # Collect moved blocks as (heading_info, block_lines) # heading_info is either None or (level, heading_text) moved_blocks: list[tuple[tuple[int, str] | None, list[str]]] = [] @@ -1064,8 +1170,11 @@ class MainWindow(QMainWindow): while i < len(lines): nxt = lines[i] - # If we're not inside a fence, a checked checkbox ends the block - if not block_in_fence and checked_re.match(nxt): + # If we're not inside a fence, a checked checkbox ends the block, + # otherwise a new heading does as well. + if not block_in_fence and ( + checked_re.match(nxt) or heading_re.match(nxt) + ): break # Normalize any unchecked checkbox lines inside the block @@ -1101,6 +1210,7 @@ class MainWindow(QMainWindow): i += 1 if moved_from_this_day: + remaining_lines = _prune_empty_headings(remaining_lines) modified_text = "\n".join(remaining_lines) # Save the cleaned-up source day self.db.save_new_version( @@ -1115,7 +1225,13 @@ class MainWindow(QMainWindow): # --- Merge all moved blocks into the *target* date --- target_text = self.db.get_entry(target_iso) or "" - target_lines = target_text.split("\n") if target_text else [] + # Treat a whitespace-only target note as truly empty; otherwise we can + # end up appending the new heading *after* leading blank lines (e.g. if + # a newly-created empty day was previously saved as just "\n"). + if not target_text.strip(): + target_lines = [] + else: + target_lines = target_text.split("\n") by_heading: dict[tuple[int, str], list[list[str]]] = {} plain_blocks: list[list[str]] = [] diff --git a/debian/changelog b/debian/changelog index 26075f4..c49574f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,11 @@ +bouquin (0.8.3) unstable; urgency=medium + + * Update urllib3 dependency to resolve CVE-2026-21441 + * Fix carrying over data to next day from over-capturing data belonging to next header section + * Other dependency updates + + -- Miguel Jacq Fri, 30 Jan 2026 16:48:00 +1100 + bouquin (0.8.2) unstable; urgency=medium * Add ability to delete an invoice via 'Manage Invoices' dialog diff --git a/poetry.lock b/poetry.lock index af05c5f..0cf8448 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,13 +14,13 @@ files = [ [[package]] name = "certifi" -version = "2025.11.12" +version = "2026.1.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" files = [ - {file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"}, - {file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"}, + {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, + {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, ] [[package]] @@ -158,103 +158,103 @@ files = [ [[package]] name = "coverage" -version = "7.13.0" +version = "7.13.2" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" files = [ - {file = "coverage-7.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:02d9fb9eccd48f6843c98a37bd6817462f130b86da8660461e8f5e54d4c06070"}, - {file = "coverage-7.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:367449cf07d33dc216c083f2036bb7d976c6e4903ab31be400ad74ad9f85ce98"}, - {file = "coverage-7.13.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cdb3c9f8fef0a954c632f64328a3935988d33a6604ce4bf67ec3e39670f12ae5"}, - {file = "coverage-7.13.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d10fd186aac2316f9bbb46ef91977f9d394ded67050ad6d84d94ed6ea2e8e54e"}, - {file = "coverage-7.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f88ae3e69df2ab62fb0bc5219a597cb890ba5c438190ffa87490b315190bb33"}, - {file = "coverage-7.13.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4be718e51e86f553bcf515305a158a1cd180d23b72f07ae76d6017c3cc5d791"}, - {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a00d3a393207ae12f7c49bb1c113190883b500f48979abb118d8b72b8c95c032"}, - {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a7b1cd820e1b6116f92c6128f1188e7afe421c7e1b35fa9836b11444e53ebd9"}, - {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:37eee4e552a65866f15dedd917d5e5f3d59805994260720821e2c1b51ac3248f"}, - {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62d7c4f13102148c78d7353c6052af6d899a7f6df66a32bddcc0c0eb7c5326f8"}, - {file = "coverage-7.13.0-cp310-cp310-win32.whl", hash = "sha256:24e4e56304fdb56f96f80eabf840eab043b3afea9348b88be680ec5986780a0f"}, - {file = "coverage-7.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:74c136e4093627cf04b26a35dab8cbfc9b37c647f0502fc313376e11726ba303"}, - {file = "coverage-7.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820"}, - {file = "coverage-7.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f"}, - {file = "coverage-7.13.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96"}, - {file = "coverage-7.13.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ed5630d946859de835a85e9a43b721123a8a44ec26e2830b296d478c7fd4259"}, - {file = "coverage-7.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f15a931a668e58087bc39d05d2b4bf4b14ff2875b49c994bbdb1c2217a8daeb"}, - {file = "coverage-7.13.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30a3a201a127ea57f7e14ba43c93c9c4be8b7d17a26e03bb49e6966d019eede9"}, - {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a485ff48fbd231efa32d58f479befce52dcb6bfb2a88bb7bf9a0b89b1bc8030"}, - {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:22486cdafba4f9e471c816a2a5745337742a617fef68e890d8baf9f3036d7833"}, - {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:263c3dbccc78e2e331e59e90115941b5f53e85cfcc6b3b2fbff1fd4e3d2c6ea8"}, - {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5330fa0cc1f5c3c4c3bb8e101b742025933e7848989370a1d4c8c5e401ea753"}, - {file = "coverage-7.13.0-cp311-cp311-win32.whl", hash = "sha256:0f4872f5d6c54419c94c25dd6ae1d015deeb337d06e448cd890a1e89a8ee7f3b"}, - {file = "coverage-7.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51a202e0f80f241ccb68e3e26e19ab5b3bf0f813314f2c967642f13ebcf1ddfe"}, - {file = "coverage-7.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:d2a9d7f1c11487b1c69367ab3ac2d81b9b3721f097aa409a3191c3e90f8f3dd7"}, - {file = "coverage-7.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf"}, - {file = "coverage-7.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f"}, - {file = "coverage-7.13.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb"}, - {file = "coverage-7.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621"}, - {file = "coverage-7.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74"}, - {file = "coverage-7.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57"}, - {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8"}, - {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d"}, - {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b"}, - {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd"}, - {file = "coverage-7.13.0-cp312-cp312-win32.whl", hash = "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef"}, - {file = "coverage-7.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae"}, - {file = "coverage-7.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080"}, - {file = "coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf"}, - {file = "coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a"}, - {file = "coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74"}, - {file = "coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6"}, - {file = "coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b"}, - {file = "coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232"}, - {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971"}, - {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d"}, - {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137"}, - {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511"}, - {file = "coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1"}, - {file = "coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a"}, - {file = "coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6"}, - {file = "coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a"}, - {file = "coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8"}, - {file = "coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053"}, - {file = "coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071"}, - {file = "coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e"}, - {file = "coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493"}, - {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0"}, - {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e"}, - {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c"}, - {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e"}, - {file = "coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46"}, - {file = "coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39"}, - {file = "coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e"}, - {file = "coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256"}, - {file = "coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a"}, - {file = "coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9"}, - {file = "coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19"}, - {file = "coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be"}, - {file = "coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb"}, - {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8"}, - {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b"}, - {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9"}, - {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927"}, - {file = "coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f"}, - {file = "coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc"}, - {file = "coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b"}, - {file = "coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28"}, - {file = "coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe"}, - {file = "coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657"}, - {file = "coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff"}, - {file = "coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3"}, - {file = "coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b"}, - {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d"}, - {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e"}, - {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940"}, - {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2"}, - {file = "coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7"}, - {file = "coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc"}, - {file = "coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a"}, - {file = "coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904"}, - {file = "coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936"}, + {file = "coverage-7.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4af3b01763909f477ea17c962e2cca8f39b350a4e46e3a30838b2c12e31b81b"}, + {file = "coverage-7.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36393bd2841fa0b59498f75466ee9bdec4f770d3254f031f23e8fd8e140ffdd2"}, + {file = "coverage-7.13.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9cc7573518b7e2186bd229b1a0fe24a807273798832c27032c4510f47ffdb896"}, + {file = "coverage-7.13.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca9566769b69a5e216a4e176d54b9df88f29d750c5b78dbb899e379b4e14b30c"}, + {file = "coverage-7.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c9bdea644e94fd66d75a6f7e9a97bb822371e1fe7eadae2cacd50fcbc28e4dc"}, + {file = "coverage-7.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5bd447332ec4f45838c1ad42268ce21ca87c40deb86eabd59888859b66be22a5"}, + {file = "coverage-7.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7c79ad5c28a16a1277e1187cf83ea8dafdcc689a784228a7d390f19776db7c31"}, + {file = "coverage-7.13.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:76e06ccacd1fb6ada5d076ed98a8c6f66e2e6acd3df02819e2ee29fd637b76ad"}, + {file = "coverage-7.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:49d49e9a5e9f4dc3d3dac95278a020afa6d6bdd41f63608a76fa05a719d5b66f"}, + {file = "coverage-7.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed2bce0e7bfa53f7b0b01c722da289ef6ad4c18ebd52b1f93704c21f116360c8"}, + {file = "coverage-7.13.2-cp310-cp310-win32.whl", hash = "sha256:1574983178b35b9af4db4a9f7328a18a14a0a0ce76ffaa1c1bacb4cc82089a7c"}, + {file = "coverage-7.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:a360a8baeb038928ceb996f5623a4cd508728f8f13e08d4e96ce161702f3dd99"}, + {file = "coverage-7.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:060ebf6f2c51aff5ba38e1f43a2095e087389b1c69d559fde6049a4b0001320e"}, + {file = "coverage-7.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1ea8ca9db5e7469cd364552985e15911548ea5b69c48a17291f0cac70484b2e"}, + {file = "coverage-7.13.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b780090d15fd58f07cf2011943e25a5f0c1c894384b13a216b6c86c8a8a7c508"}, + {file = "coverage-7.13.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:88a800258d83acb803c38175b4495d293656d5fac48659c953c18e5f539a274b"}, + {file = "coverage-7.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6326e18e9a553e674d948536a04a80d850a5eeefe2aae2e6d7cf05d54046c01b"}, + {file = "coverage-7.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59562de3f797979e1ff07c587e2ac36ba60ca59d16c211eceaa579c266c5022f"}, + {file = "coverage-7.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:27ba1ed6f66b0e2d61bfa78874dffd4f8c3a12f8e2b5410e515ab345ba7bc9c3"}, + {file = "coverage-7.13.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8be48da4d47cc68754ce643ea50b3234557cbefe47c2f120495e7bd0a2756f2b"}, + {file = "coverage-7.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2a47a4223d3361b91176aedd9d4e05844ca67d7188456227b6bf5e436630c9a1"}, + {file = "coverage-7.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6f141b468740197d6bd38f2b26ade124363228cc3f9858bd9924ab059e00059"}, + {file = "coverage-7.13.2-cp311-cp311-win32.whl", hash = "sha256:89567798404af067604246e01a49ef907d112edf2b75ef814b1364d5ce267031"}, + {file = "coverage-7.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:21dd57941804ae2ac7e921771a5e21bbf9aabec317a041d164853ad0a96ce31e"}, + {file = "coverage-7.13.2-cp311-cp311-win_arm64.whl", hash = "sha256:10758e0586c134a0bafa28f2d37dd2cdb5e4a90de25c0fc0c77dabbad46eca28"}, + {file = "coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d"}, + {file = "coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3"}, + {file = "coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99"}, + {file = "coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f"}, + {file = "coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f"}, + {file = "coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa"}, + {file = "coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce"}, + {file = "coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94"}, + {file = "coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5"}, + {file = "coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b"}, + {file = "coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41"}, + {file = "coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e"}, + {file = "coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894"}, + {file = "coverage-7.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6"}, + {file = "coverage-7.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc"}, + {file = "coverage-7.13.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f"}, + {file = "coverage-7.13.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1"}, + {file = "coverage-7.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9"}, + {file = "coverage-7.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c"}, + {file = "coverage-7.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5"}, + {file = "coverage-7.13.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4"}, + {file = "coverage-7.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c"}, + {file = "coverage-7.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31"}, + {file = "coverage-7.13.2-cp313-cp313-win32.whl", hash = "sha256:5b20211c47a8abf4abc3319d8ce2464864fa9f30c5fcaf958a3eed92f4f1fef8"}, + {file = "coverage-7.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:14f500232e521201cf031549fb1ebdfc0a40f401cf519157f76c397e586c3beb"}, + {file = "coverage-7.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:9779310cb5a9778a60c899f075a8514c89fa6d10131445c2207fc893e0b14557"}, + {file = "coverage-7.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e"}, + {file = "coverage-7.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7"}, + {file = "coverage-7.13.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3"}, + {file = "coverage-7.13.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3"}, + {file = "coverage-7.13.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421"}, + {file = "coverage-7.13.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5"}, + {file = "coverage-7.13.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23"}, + {file = "coverage-7.13.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c"}, + {file = "coverage-7.13.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f"}, + {file = "coverage-7.13.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573"}, + {file = "coverage-7.13.2-cp313-cp313t-win32.whl", hash = "sha256:14ae4146465f8e6e6253eba0cccd57423e598a4cb925958b240c805300918343"}, + {file = "coverage-7.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9074896edd705a05769e3de0eac0a8388484b503b68863dd06d5e473f874fd47"}, + {file = "coverage-7.13.2-cp313-cp313t-win_arm64.whl", hash = "sha256:69e526e14f3f854eda573d3cf40cffd29a1a91c684743d904c33dbdcd0e0f3e7"}, + {file = "coverage-7.13.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef"}, + {file = "coverage-7.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f"}, + {file = "coverage-7.13.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5"}, + {file = "coverage-7.13.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4"}, + {file = "coverage-7.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27"}, + {file = "coverage-7.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548"}, + {file = "coverage-7.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660"}, + {file = "coverage-7.13.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92"}, + {file = "coverage-7.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82"}, + {file = "coverage-7.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892"}, + {file = "coverage-7.13.2-cp314-cp314-win32.whl", hash = "sha256:9b2f4714bb7d99ba3790ee095b3b4ac94767e1347fe424278a0b10acb3ff04fe"}, + {file = "coverage-7.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4121a90823a063d717a96e0a0529c727fb31ea889369a0ee3ec00ed99bf6859"}, + {file = "coverage-7.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:6873f0271b4a15a33e7590f338d823f6f66f91ed147a03938d7ce26efd04eee6"}, + {file = "coverage-7.13.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b"}, + {file = "coverage-7.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417"}, + {file = "coverage-7.13.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee"}, + {file = "coverage-7.13.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1"}, + {file = "coverage-7.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d"}, + {file = "coverage-7.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6"}, + {file = "coverage-7.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a"}, + {file = "coverage-7.13.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04"}, + {file = "coverage-7.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f"}, + {file = "coverage-7.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f"}, + {file = "coverage-7.13.2-cp314-cp314t-win32.whl", hash = "sha256:79f6506a678a59d4ded048dc72f1859ebede8ec2b9a2d509ebe161f01c2879d3"}, + {file = "coverage-7.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:196bfeabdccc5a020a57d5a368c681e3a6ceb0447d153aeccc1ab4d70a5032ba"}, + {file = "coverage-7.13.2-cp314-cp314t-win_arm64.whl", hash = "sha256:69269ab58783e090bfbf5b916ab3d188126e22d6070bbfc93098fdd474ef937c"}, + {file = "coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5"}, + {file = "coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3"}, ] [package.dependencies] @@ -321,28 +321,28 @@ files = [ [[package]] name = "markdown" -version = "3.10" +version = "3.10.1" description = "Python implementation of John Gruber's Markdown." optional = false python-versions = ">=3.10" files = [ - {file = "markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c"}, - {file = "markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e"}, + {file = "markdown-3.10.1-py3-none-any.whl", hash = "sha256:867d788939fe33e4b736426f5b9f651ad0c0ae0ecf89df0ca5d1176c70812fe3"}, + {file = "markdown-3.10.1.tar.gz", hash = "sha256:1c19c10bd5c14ac948c53d0d762a04e2fa35a6d58a6b7b1e6bfcbe6fefc0001a"}, ] [package.extras] -docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python] (>=0.28.3)"] testing = ["coverage", "pyyaml"] [[package]] name = "packaging" -version = "25.0" +version = "26.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, ] [[package]] @@ -560,53 +560,58 @@ files = [ [[package]] name = "tomli" -version = "2.3.0" +version = "2.4.0" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" files = [ - {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, - {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, - {file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"}, - {file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"}, - {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"}, - {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"}, - {file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"}, - {file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"}, - {file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"}, - {file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"}, - {file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"}, - {file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"}, - {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"}, - {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"}, - {file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"}, - {file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"}, - {file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"}, - {file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"}, - {file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"}, - {file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"}, - {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"}, - {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"}, - {file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"}, - {file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"}, - {file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"}, - {file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"}, - {file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"}, - {file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"}, - {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"}, - {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"}, - {file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"}, - {file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"}, - {file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"}, - {file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"}, - {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"}, - {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"}, - {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"}, - {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"}, - {file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"}, - {file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"}, - {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}, - {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, + {file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"}, + {file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"}, + {file = "tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"}, + {file = "tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"}, + {file = "tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"}, + {file = "tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"}, + {file = "tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"}, + {file = "tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f"}, + {file = "tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86"}, + {file = "tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87"}, + {file = "tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8"}, + {file = "tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776"}, + {file = "tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475"}, + {file = "tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b"}, + {file = "tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087"}, + {file = "tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd"}, + {file = "tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4"}, + {file = "tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"}, + {file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 868fb26..0eef382 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "bouquin" -version = "0.8.2" +version = "0.8.3" description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher." authors = ["Miguel Jacq "] readme = "README.md" diff --git a/rpm/bouquin.spec b/rpm/bouquin.spec index 2f8d442..d50d461 100644 --- a/rpm/bouquin.spec +++ b/rpm/bouquin.spec @@ -4,7 +4,7 @@ # provides the Python distribution/module as "sqlcipher4". To keep Fedora's # auto-generated python3dist() Requires correct, we rewrite the dependency key in # pyproject.toml at build time. -%global upstream_version 0.8.2 +%global upstream_version 0.8.3 Name: bouquin Version: %{upstream_version} @@ -82,6 +82,10 @@ install -Dpm 0644 bouquin/icons/bouquin.svg %{buildroot}%{_datadir}/icons/hicolo %{_datadir}/icons/hicolor/scalable/apps/bouquin.svg %changelog +* Fri Jan 30 2026 Miguel Jacq - %{version}-%{release} +- Update urllib3 dependency to resolve CVE-2026-21441 +- Fix carrying over data to next day from over-capturing data belonging to next header section +- Other dependency updates * Wed Dec 31 2025 Miguel Jacq - %{version}-%{release} - Add ability to delete an invoice via 'Manage Invoices' dialog * Fri Dec 26 2025 Miguel Jacq - %{version}-%{release} From 5bb61273da38ba0554fa08676474a9d799a0167c Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 30 Jan 2026 17:03:38 +1100 Subject: [PATCH 17/17] Fix version change --- release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.sh b/release.sh index 63e2723..0d3945e 100755 --- a/release.sh +++ b/release.sh @@ -115,4 +115,4 @@ rsync -aHPvz --exclude=.git --delete "$REPO_ROOT/" "$REMOTE/" echo "Done!" -ssh lupin.mig5.net "echo ${VERSION} | tee /opt/www/mig5.net/bouquin/version.txt" +ssh lupin.mig5.net "echo ${VERSION} | tee /var/www/bouquin/version.txt"