Compare commits
No commits in common. "main" and "0.7.5" have entirely different histories.
29 changed files with 640 additions and 2797 deletions
|
|
@ -1,80 +0,0 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: docker
|
||||
|
||||
steps:
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl gnupg2 ca-certificates
|
||||
mkdir -p /usr/share/keyrings
|
||||
curl -fsSL https://mig5.net/static/mig5.asc | gpg --dearmor -o /usr/share/keyrings/mig5.gpg
|
||||
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/mig5.gpg] https://apt.mig5.net trixie main" | tee /etc/apt/sources.list.d/mig5.list
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
devscripts \
|
||||
debhelper \
|
||||
dh-python \
|
||||
python3-all-dev \
|
||||
python3-setuptools \
|
||||
python3-wheel \
|
||||
libssl-dev \
|
||||
rsync \
|
||||
pybuild-plugin-pyproject \
|
||||
python3-poetry-core \
|
||||
python3-sqlcipher4 \
|
||||
python3-pyside6.qtwidgets \
|
||||
python3-pyside6.qtcore \
|
||||
python3-pyside6.qtgui \
|
||||
python3-pyside6.qtsvg \
|
||||
python3-pyside6.qtprintsupport \
|
||||
python3-requests \
|
||||
python3-markdown \
|
||||
libxcb-cursor0 \
|
||||
fonts-noto-core
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Build deb
|
||||
run: |
|
||||
mkdir /out
|
||||
|
||||
rsync -a --delete \
|
||||
--exclude '.git' \
|
||||
--exclude '.venv' \
|
||||
--exclude 'dist' \
|
||||
--exclude 'build' \
|
||||
--exclude '__pycache__' \
|
||||
--exclude '.pytest_cache' \
|
||||
--exclude '.mypy_cache' \
|
||||
./ /out/
|
||||
|
||||
cd /out/
|
||||
export DEBEMAIL="mig@mig5.net"
|
||||
export DEBFULLNAME="Miguel Jacq"
|
||||
|
||||
dch --distribution "trixie" --local "~trixie" "CI build for trixie"
|
||||
dpkg-buildpackage -us -uc -b
|
||||
|
||||
# Notify if any previous step in this job failed
|
||||
- name: Notify on failure
|
||||
if: ${{ failure() }}
|
||||
env:
|
||||
WEBHOOK_URL: ${{ secrets.NODERED_WEBHOOK_URL }}
|
||||
REPOSITORY: ${{ forgejo.repository }}
|
||||
RUN_NUMBER: ${{ forgejo.run_number }}
|
||||
SERVER_URL: ${{ forgejo.server_url }}
|
||||
run: |
|
||||
curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"repository\":\"$REPOSITORY\",\"run_number\":\"$RUN_NUMBER\",\"status\":\"failure\",\"url\":\"$SERVER_URL/$REPOSITORY/actions/runs/$RUN_NUMBER\"}" \
|
||||
"$WEBHOOK_URL"
|
||||
|
|
@ -23,7 +23,7 @@ jobs:
|
|||
|
||||
- name: Run trivy
|
||||
run: |
|
||||
trivy fs --no-progress --ignore-unfixed --format table --disable-telemetry --skip-version-check --exit-code 1 .
|
||||
trivy fs --no-progress --ignore-unfixed --format table --disable-telemetry .
|
||||
|
||||
# Notify if any previous step in this job failed
|
||||
- name: Notify on failure
|
||||
|
|
|
|||
26
CHANGELOG.md
26
CHANGELOG.md
|
|
@ -1,29 +1,3 @@
|
|||
# 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
|
||||
|
||||
* 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.
|
||||
* 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
|
||||
|
||||
* Add .desktop file for Debian
|
||||
* Fix Pomodoro timer rounding so it rounds up to 0.25, but rounds to closest quarter (up or down) for minutes higher than that, instead of always up to next quarter.
|
||||
* Allow setting a code block on a line that already has text (it will start a newline for the codeblock)
|
||||
* Retain indentation when tab is used to indent a line, unless enter is pressed twice or user deletes the indentation
|
||||
* Add ability to collapse/expand sections of text.
|
||||
* Add 'Last Month' date range for timesheet reports
|
||||
* Add missing strings (for English and French)
|
||||
* Don't offer to download latest AppImage unless we are running as an AppImage already
|
||||
|
||||
# 0.7.5
|
||||
|
||||
* Fix import of sqlcipher4
|
||||
|
|
|
|||
|
|
@ -1,110 +0,0 @@
|
|||
# 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}"
|
||||
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)-)' | grep "${VERSION_ID}")
|
||||
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 <dir>:/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"]
|
||||
54
README.md
54
README.md
|
|
@ -12,11 +12,6 @@ It is designed to treat each day as its own 'page', complete with Markdown rende
|
|||
search, reminders and time logging for those of us who need to keep track of not just TODOs, but
|
||||
also how long we spent on them.
|
||||
|
||||
For those who rely on that time logging for work, there is also an Invoicing feature that can
|
||||
generate invoices of that time spent.
|
||||
|
||||
There is also support for embedding documents in a file manager.
|
||||
|
||||
It uses SQLCipher as a drop-in replacement for SQLite3.
|
||||
|
||||
This means that the underlying database for the notebook is encrypted at rest.
|
||||
|
|
@ -57,18 +52,16 @@ report from within the app, or optionally to check for new versions to upgrade t
|
|||
</div>
|
||||
|
||||
|
||||
## Features
|
||||
## Some of the features
|
||||
|
||||
* Data is encrypted at rest
|
||||
* Encryption key is prompted for and never stored, unless user chooses to via Settings
|
||||
* All changes are version controlled, with ability to view/diff versions, revert or delete revisions
|
||||
* Tabs are supported - right-click on a date from the calendar to open it in a new tab.
|
||||
* Automatic rendering of basic Markdown syntax
|
||||
* Basic code block editing/highlighting
|
||||
* Ability to collapse/expand sections of text
|
||||
* Ability to increase/decrease font size
|
||||
* Tabs are supported - right-click on a date from the calendar to open it in a new tab.
|
||||
* Images are supported
|
||||
* Search all pages, or find text on current page
|
||||
* Add and manage tags
|
||||
* Automatic periodic saving (or explicitly save)
|
||||
* Automatic locking of the app after a period of inactivity (default 15 min)
|
||||
* Rekey the database (change the password)
|
||||
|
|
@ -76,20 +69,18 @@ report from within the app, or optionally to check for new versions to upgrade t
|
|||
* Backup the database to encrypted SQLCipher format (which can then be loaded back in to a Bouquin)
|
||||
* Dark and light theme support
|
||||
* Automatically generate checkboxes when typing 'TODO'
|
||||
* It is possible to automatically move unchecked checkboxes from the last 7 days to the next day.
|
||||
* It is possible to automatically move unchecked checkboxes from the last 7 days to the next weekday.
|
||||
* English, French and Italian locales provided
|
||||
* Ability to set reminder alarms (which will be flashed as the reminder or can be sent as webhooks/email notifications)
|
||||
* Ability to log time per day for different projects/activities, pomodoro-style log timer, timesheet reports and invoicing of time spent
|
||||
* Ability to set reminder alarms (which will be flashed as the reminder)
|
||||
* Ability to log time per day for different projects/activities, pomodoro-style log timer and timesheet reports
|
||||
* Ability to store and tag documents (tied to Projects, same as the Time Logging system). The documents are stored embedded in the encrypted database.
|
||||
* Add and manage tags on pages and documents
|
||||
|
||||
|
||||
## How to install
|
||||
|
||||
Unless you are using the Debian option below:
|
||||
Make sure you have `libxcb-cursor0` installed (on Debian-based distributions) or `xcb-util-cursor` (RedHat/Fedora-based distributions).
|
||||
|
||||
* Make sure you have `libxcb-cursor0` installed (on Debian-based distributions) or `xcb-util-cursor` (RedHat/Fedora-based distributions).
|
||||
* If downloading from my Forgejo's Releases page, you may wish to verify the GPG signatures with my [GPG key](https://mig5.net/static/mig5.asc).
|
||||
If downloading from my Forgejo's Releases page, you may wish to verify the GPG signatures with my [GPG key](https://mig5.net/static/mig5.asc).
|
||||
|
||||
### Debian 13 ('Trixie')
|
||||
|
||||
|
|
@ -101,24 +92,6 @@ 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/$releasever/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
|
||||
|
||||
|
|
@ -135,4 +108,13 @@ sudo dnf install bouquin
|
|||
* Run `poetry install` to install dependencies
|
||||
* Run `poetry run bouquin` to start the application.
|
||||
|
||||
Alternatively, you can download the source code and wheels from Releases as well.
|
||||
### From the releases page
|
||||
|
||||
* Download the whl and run it
|
||||
|
||||
## How to run the tests
|
||||
|
||||
* Clone the repo
|
||||
* Ensure you have poetry installed
|
||||
* Run `poetry install --with test`
|
||||
* Run `./tests.sh`
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from PySide6.QtCore import QRect, QSize, Qt
|
||||
from PySide6.QtGui import QColor, QFont, QFontMetrics, QPainter, QPalette, QTextCursor
|
||||
from PySide6.QtGui import QColor, QFont, QFontMetrics, QPainter, QPalette
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox,
|
||||
QDialog,
|
||||
|
|
@ -34,12 +32,6 @@ class CodeEditorWithLineNumbers(QPlainTextEdit):
|
|||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
# Allow Tab to insert indentation (not move focus between widgets)
|
||||
self.setTabChangesFocus(False)
|
||||
|
||||
# Track whether we just auto-inserted indentation on Enter
|
||||
self._last_enter_was_empty_indent = False
|
||||
|
||||
self._line_number_area = _LineNumberArea(self)
|
||||
|
||||
self.blockCountChanged.connect(self._update_line_number_area_width)
|
||||
|
|
@ -148,48 +140,6 @@ class CodeEditorWithLineNumbers(QPlainTextEdit):
|
|||
bottom = top + self.blockBoundingRect(block).height()
|
||||
block_number += 1
|
||||
|
||||
def keyPressEvent(self, event): # type: ignore[override]
|
||||
"""Auto-retain indentation on newlines (Tab/space) like the markdown editor.
|
||||
|
||||
Rules:
|
||||
- If the current line is indented, Enter inserts a newline + the same indent.
|
||||
- If the current line contains only indentation, a *second* Enter clears the indent
|
||||
and starts an unindented line (similar to exiting bullets/checkboxes).
|
||||
"""
|
||||
if event.key() in (Qt.Key_Return, Qt.Key_Enter):
|
||||
cursor = self.textCursor()
|
||||
block_text = cursor.block().text()
|
||||
indent = re.match(r"[ \t]*", block_text).group(0) # type: ignore[union-attr]
|
||||
|
||||
if indent:
|
||||
rest = block_text[len(indent) :]
|
||||
indent_only = rest.strip() == ""
|
||||
|
||||
if indent_only and self._last_enter_was_empty_indent:
|
||||
# Second Enter on an indentation-only line: remove that line and
|
||||
# start a fresh, unindented line.
|
||||
cursor.select(QTextCursor.SelectionType.LineUnderCursor)
|
||||
cursor.removeSelectedText()
|
||||
cursor.insertText("\n")
|
||||
self.setTextCursor(cursor)
|
||||
self._last_enter_was_empty_indent = False
|
||||
return
|
||||
|
||||
# First Enter: keep indentation
|
||||
super().keyPressEvent(event)
|
||||
self.textCursor().insertText(indent)
|
||||
self._last_enter_was_empty_indent = True
|
||||
return
|
||||
|
||||
# No indent -> normal Enter
|
||||
self._last_enter_was_empty_indent = False
|
||||
super().keyPressEvent(event)
|
||||
return
|
||||
|
||||
# Any other key resets the empty-indent-enter flag
|
||||
self._last_enter_was_empty_indent = False
|
||||
super().keyPressEvent(event)
|
||||
|
||||
|
||||
class CodeBlockEditorDialog(QDialog):
|
||||
def __init__(
|
||||
|
|
|
|||
|
|
@ -2392,18 +2392,6 @@ 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,
|
||||
|
|
|
|||
|
|
@ -1065,10 +1065,6 @@ 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)
|
||||
|
|
@ -1077,68 +1073,6 @@ 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:
|
||||
|
|
|
|||
|
|
@ -217,7 +217,6 @@
|
|||
"date_range": "Date range",
|
||||
"custom_range": "Custom",
|
||||
"last_week": "Last week",
|
||||
"last_month": "Last month",
|
||||
"this_week": "This week",
|
||||
"this_month": "This month",
|
||||
"this_year": "This year",
|
||||
|
|
@ -303,10 +302,6 @@
|
|||
"cut": "Cut",
|
||||
"copy": "Copy",
|
||||
"paste": "Paste",
|
||||
"collapse": "Collapse",
|
||||
"expand": "Expand",
|
||||
"remove_collapse": "Remove collapse",
|
||||
"collapse_selection": "Collapse selection",
|
||||
"start": "Start",
|
||||
"pause": "Pause",
|
||||
"resume": "Resume",
|
||||
|
|
@ -368,19 +363,21 @@
|
|||
"documents_col_file": "File",
|
||||
"documents_col_description": "Description",
|
||||
"documents_col_added": "Added",
|
||||
"documents_col_path": "Path",
|
||||
"documents_col_tags": "Tags",
|
||||
"documents_col_size": "Size",
|
||||
"documents_add": "&Add",
|
||||
"documents_add_document": "Add a document",
|
||||
"documents_open": "&Open",
|
||||
"documents_delete": "&Delete",
|
||||
"documents_no_project_selected": "Please choose a project first.",
|
||||
"documents_file_filter_all": "All files (*)",
|
||||
"documents_add_failed": "Could not add document: {error}",
|
||||
"documents_open_failed": "Could not open document: {error}",
|
||||
"documents_missing_file": "The file does not exist:\n{path}",
|
||||
"documents_confirm_delete": "Remove this document from the project?\n(The file on disk will not be deleted.)",
|
||||
"documents_search_label": "Search",
|
||||
"documents_search_placeholder": "Type to search documents (all projects)",
|
||||
"documents_invalid_date_format": "Invalid date format",
|
||||
"todays_documents": "Documents from this day",
|
||||
"todays_documents_none": "No documents yet.",
|
||||
"manage_invoices": "Manage Invoices",
|
||||
|
|
@ -431,11 +428,5 @@
|
|||
"invoice_company_logo_choose": "Choose logo",
|
||||
"invoice_company_logo_set": "Logo has been set",
|
||||
"invoice_company_logo_not_set": "Logo not set",
|
||||
"invoice_number_unique": "Invoice number must be unique. This invoice number already exists.",
|
||||
"invoice_invalid_amount": "The amount is invalid",
|
||||
"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_required": "Please select a specific invoice before trying to delete an invoice."
|
||||
"invoice_number_unique": "Invoice number must be unique. This invoice number already exists."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,8 +40,6 @@
|
|||
"next_day": "Jour suivant",
|
||||
"today": "Aujourd'hui",
|
||||
"show": "Afficher",
|
||||
"edit": "Modifier",
|
||||
"delete": "Supprimer",
|
||||
"history": "Historique",
|
||||
"export_accessible_flag": "E&xporter",
|
||||
"export_entries": "Exporter les entrées",
|
||||
|
|
@ -103,7 +101,6 @@
|
|||
"autosave": "enregistrement automatique",
|
||||
"unchecked_checkbox_items_moved_to_next_day": "Les cases non cochées ont été reportées au jour suivant",
|
||||
"move_unchecked_todos_to_today_on_startup": "Déplacer automatiquement les TODO non cochés\ndes 7 derniers jours vers le prochain jour ouvrable",
|
||||
"move_todos_include_weekends": "Autoriser le déplacement des TODO non cochées vers un week-end\nplutôt que vers le prochain jour ouvrable",
|
||||
"insert_images": "Insérer des images",
|
||||
"images": "Images",
|
||||
"reopen_failed": "Échec de la réouverture",
|
||||
|
|
@ -145,7 +142,6 @@
|
|||
"tag_browser_instructions": "Cliquez sur une étiquette pour l'étendre et voir toutes les pages avec cette étiquette. Cliquez sur une date pour l'ouvrir. Sélectionnez une étiquette pour modifier son nom, changer sa couleur ou la supprimer globalement.",
|
||||
"color_hex": "Couleur",
|
||||
"date": "Date",
|
||||
"page_or_document": "Page / Document",
|
||||
"add_a_tag": "Ajouter une étiquette",
|
||||
"edit_tag_name": "Modifier le nom de l'étiquette",
|
||||
"new_tag_name": "Nouveau nom de l'étiquette :",
|
||||
|
|
@ -155,11 +151,6 @@
|
|||
"tag_already_exists_with_that_name": "Une étiquette portant ce nom existe déjà",
|
||||
"statistics": "Statistiques",
|
||||
"main_window_statistics_accessible_flag": "Stat&istiques",
|
||||
"stats_group_pages": "Pages",
|
||||
"stats_group_tags": "Étiquettes",
|
||||
"stats_group_documents": "Documents",
|
||||
"stats_group_time_logging": "Journal de temps",
|
||||
"stats_group_reminders": "Rappels",
|
||||
"stats_pages_with_content": "Pages avec contenu (version actuelle)",
|
||||
"stats_total_revisions": "Nombre total de révisions",
|
||||
"stats_page_most_revisions": "Page avec le plus de révisions",
|
||||
|
|
@ -170,18 +161,7 @@
|
|||
"stats_heatmap_metric": "Colorer selon",
|
||||
"stats_metric_words": "Mots",
|
||||
"stats_metric_revisions": "Révisions",
|
||||
"stats_metric_documents": "Documents",
|
||||
"stats_total_documents": "Total des documents",
|
||||
"stats_date_most_documents": "Date avec le plus de documents",
|
||||
"stats_no_data": "Aucune statistique disponible pour le moment.",
|
||||
"stats_time_total_hours": "Total des heures enregistrées",
|
||||
"stats_time_day_most_hours": "Jour avec le plus d'heures enregistrées",
|
||||
"stats_time_project_most_hours": "Projet avec le plus d'heures enregistrées",
|
||||
"stats_time_activity_most_hours": "Activité avec le plus d'heures enregistrées",
|
||||
"stats_total_reminders": "Total des rappels",
|
||||
"stats_date_most_reminders": "Jour avec le plus de rappels",
|
||||
"stats_metric_hours": "Heures",
|
||||
"stats_metric_reminders": "Rappels",
|
||||
"select_notebook": "Sélectionner un bouquin",
|
||||
"bug_report_explanation": "Décrivez ce qui s'est mal passé, ce que vous attendiez et les étapes pour reproduire le problème.\n\nNous ne collectons rien d'autre que le numéro de version de Bouquin.\n\nSi vous souhaitez être contacté, veuillez laisser vos coordonnées.\n\nVotre demande sera envoyée via HTTPS.",
|
||||
"bug_report_placeholder": "Saisissez votre rapport de bug ici",
|
||||
|
|
@ -209,19 +189,10 @@
|
|||
"add_project": "Ajouter un projet",
|
||||
"add_time_entry": "Ajouter une entrée de temps",
|
||||
"time_period": "Période",
|
||||
"dont_group": "Ne pas regrouper",
|
||||
"by_activity": "par activité",
|
||||
"by_day": "par jour",
|
||||
"by_month": "par mois",
|
||||
"by_week": "par semaine",
|
||||
"date_range": "Plage de dates",
|
||||
"custom_range": "Personnalisé",
|
||||
"last_week": "La semaine dernière",
|
||||
"last_month": "Le mois dernier",
|
||||
"this_week": "Cette semaine",
|
||||
"this_month": "Ce mois-ci",
|
||||
"this_year": "Cette année",
|
||||
"all_projects": "Tous les projets",
|
||||
"delete_activity": "Supprimer l'activité",
|
||||
"delete_activity_confirm": "Êtes-vous sûr de vouloir supprimer cette activité ?",
|
||||
"delete_activity_title": "Supprimer l'activité - êtes-vous sûr ?",
|
||||
|
|
@ -231,7 +202,6 @@
|
|||
"delete_time_entry": "Supprimer l'entrée de temps",
|
||||
"group_by": "Grouper par",
|
||||
"hours": "Heures",
|
||||
"created_at": "Créé le",
|
||||
"invalid_activity_message": "L'activité est invalide",
|
||||
"invalid_activity_title": "Activité invalide",
|
||||
"invalid_project_message": "Le projet est invalide",
|
||||
|
|
@ -250,8 +220,6 @@
|
|||
"projects": "Projets",
|
||||
"rename_activity": "Renommer l'activité",
|
||||
"rename_project": "Renommer le projet",
|
||||
"reporting": "Rapports",
|
||||
"reporting_and_invoicing": "Rapports et facturation",
|
||||
"run_report": "Exécuter le rapport",
|
||||
"add_activity_title": "Ajouter une activité",
|
||||
"add_activity_label": "Ajouter une activité",
|
||||
|
|
@ -267,10 +235,8 @@
|
|||
"select_project_title": "Sélectionner un projet",
|
||||
"time_log": "Journal de temps",
|
||||
"time_log_collapsed_hint": "Journal de temps",
|
||||
"date_label": "Date : {date}",
|
||||
"change_date": "Modifier la date",
|
||||
"select_date_title": "Sélectionner une date",
|
||||
"for": "Pour {date}",
|
||||
"time_log_date_label": "Date du journal de temps : {date}",
|
||||
"time_log_for": "Journal de temps pour {date}",
|
||||
"time_log_no_date": "Journal de temps",
|
||||
"time_log_no_entries": "Aucune entrée de temps pour l'instant",
|
||||
"time_log_report": "Rapport de temps",
|
||||
|
|
@ -292,50 +258,26 @@
|
|||
"enable_tags_feature": "Activer les étiquettes",
|
||||
"enable_time_log_feature": "Activer le journal de temps",
|
||||
"enable_reminders_feature": "Activer les rappels",
|
||||
"reminders_webhook_section_title": "Envoyer les rappels vers un webhook",
|
||||
"reminders_webhook_url_label": "URL du webhook",
|
||||
"reminders_webhook_secret_label": "Secret du webhook (envoyé dans l'en-tête\nX-Bouquin-Secret)",
|
||||
"enable_documents_feature": "Activer le stockage des documents",
|
||||
"pomodoro_time_log_default_text": "Session de concentration",
|
||||
"toolbar_pomodoro_timer": "Minuteur de suivi du temps",
|
||||
"set_code_language": "Définir le langage du code",
|
||||
"cut": "Couper",
|
||||
"copy": "Copier",
|
||||
"paste": "Coller",
|
||||
"collapse": "Replier",
|
||||
"expand": "Déplier",
|
||||
"remove_collapse": "Supprimer le pliage",
|
||||
"collapse_selection": "Replier la sélection",
|
||||
"start": "Démarrer",
|
||||
"pause": "Pause",
|
||||
"resume": "Reprendre",
|
||||
"stop_and_log": "Arrêter et enregistrer",
|
||||
"manage_reminders": "Gérer les rappels",
|
||||
"upcoming_reminders": "Rappels à venir",
|
||||
"no_upcoming_reminders": "Aucun rappel à venir",
|
||||
"once": "Une fois (aujourd'hui)",
|
||||
"once": "une fois",
|
||||
"daily": "quotidien",
|
||||
"weekdays": "jours de semaine",
|
||||
"weekly": "hebdomadaire",
|
||||
"add_reminder": "Ajouter un rappel",
|
||||
"edit_reminder": "Modifier le rappel",
|
||||
"delete_reminder": "Supprimer le rappel",
|
||||
"delete_reminders": "Supprimer les rappels",
|
||||
"deleting_it_will_remove_all_future_occurrences": "La suppression supprimera toutes les occurrences futures.",
|
||||
"this_is_a_reminder_of_type": "Note : il s'agit d'un rappel de type",
|
||||
"this_will_delete_the_actual_reminders": "Note : cela supprimera les rappels eux-mêmes, pas seulement des occurrences individuelles.",
|
||||
"reminders": "Rappels",
|
||||
"time": "Heure",
|
||||
"once": "Une fois (aujourd'hui)",
|
||||
"every_day": "Tous les jours",
|
||||
"every_weekday": "Tous les jours de semaine (lun-ven)",
|
||||
"every_week": "Toutes les semaines",
|
||||
"every_fortnight": "Toutes les 2 semaines",
|
||||
"every_month": "Chaque mois (même date)",
|
||||
"every_month_nth_weekday": "Chaque mois (ex. 3e lundi)",
|
||||
"week_in_month": "Semaine du mois",
|
||||
"fortnightly": "Toutes les deux semaines",
|
||||
"monthly_same_date": "Mensuel (même date)",
|
||||
"monthly_nth_weekday": "Mensuel (nᵉ jour de semaine)",
|
||||
"repeat": "Répéter",
|
||||
"monday": "Lundi",
|
||||
"tuesday": "Mardi",
|
||||
|
|
@ -344,94 +286,5 @@
|
|||
"friday": "Vendredi",
|
||||
"saturday": "Samedi",
|
||||
"sunday": "Dimanche",
|
||||
"monday_short": "Lun",
|
||||
"tuesday_short": "Mar",
|
||||
"wednesday_short": "Mer",
|
||||
"thursday_short": "Jeu",
|
||||
"friday_short": "Ven",
|
||||
"saturday_short": "Sam",
|
||||
"sunday_short": "Dim",
|
||||
"day": "Jour",
|
||||
"text": "Texte",
|
||||
"type": "Type",
|
||||
"active": "Actif",
|
||||
"actions": "Actions",
|
||||
"edit_code_block": "Modifier le bloc de code",
|
||||
"delete_code_block": "Supprimer le bloc de code",
|
||||
"search_result_heading_document": "Document",
|
||||
"toolbar_documents": "Gestionnaire de documents",
|
||||
"project_documents_title": "Documents du projet",
|
||||
"documents_col_file": "Fichier",
|
||||
"documents_col_description": "Description",
|
||||
"documents_col_added": "Ajouté",
|
||||
"documents_col_tags": "Étiquettes",
|
||||
"documents_col_size": "Taille",
|
||||
"documents_add": "&Ajouter",
|
||||
"documents_open": "&Ouvrir",
|
||||
"documents_delete": "&Supprimer",
|
||||
"documents_no_project_selected": "Veuillez d'abord choisir un projet.",
|
||||
"documents_file_filter_all": "Tous les fichiers (*)",
|
||||
"documents_add_failed": "Impossible d'ajouter le document : {error}",
|
||||
"documents_open_failed": "Impossible d'ouvrir le document : {error}",
|
||||
"documents_confirm_delete": "Retirer ce document du projet ?\n(Le fichier sur le disque ne sera pas supprimé.)",
|
||||
"documents_search_label": "Rechercher",
|
||||
"documents_search_placeholder": "Saisir pour rechercher des documents (tous les projets)",
|
||||
"documents_invalid_date_format": "Format de date invalide",
|
||||
"todays_documents": "Documents de ce jour",
|
||||
"todays_documents_none": "Aucun document pour le moment.",
|
||||
"manage_invoices": "Gérer les factures",
|
||||
"create_invoice": "Créer une facture",
|
||||
"invoice_amount": "Montant",
|
||||
"invoice_apply_tax": "Appliquer la taxe",
|
||||
"invoice_client_address": "Adresse du client",
|
||||
"invoice_client_company": "Société cliente",
|
||||
"invoice_client_email": "E-mail du client",
|
||||
"invoice_client_name": "Contact client",
|
||||
"invoice_currency": "Devise",
|
||||
"invoice_dialog_title": "Créer une facture",
|
||||
"invoice_due_date": "Date d'échéance",
|
||||
"invoice_hourly_rate": "Taux horaire",
|
||||
"invoice_hours": "Heures",
|
||||
"invoice_issue_date": "Date d'émission",
|
||||
"invoice_mode_detailed": "Mode détaillé",
|
||||
"invoice_mode_summary": "Mode récapitulatif",
|
||||
"invoice_number": "Numéro de facture",
|
||||
"invoice_save_and_export": "Enregistrer et exporter",
|
||||
"invoice_save_pdf_title": "Enregistrer le PDF",
|
||||
"invoice_subtotal": "Sous-total",
|
||||
"invoice_summary_default_desc": "Services de conseil pour le mois de",
|
||||
"invoice_summary_desc": "Description du récapitulatif",
|
||||
"invoice_summary_hours": "Heures du récapitulatif",
|
||||
"invoice_tax": "Détails de la taxe",
|
||||
"invoice_tax_label": "Type de taxe",
|
||||
"invoice_tax_rate": "Taux de taxe",
|
||||
"invoice_tax_total": "Total des taxes",
|
||||
"invoice_total": "Total",
|
||||
"invoice_paid_at": "Payée le",
|
||||
"invoice_payment_note": "Notes de paiement",
|
||||
"invoice_project_required_title": "Projet requis",
|
||||
"invoice_project_required_message": "Veuillez sélectionner un projet spécifique avant d'essayer de créer une facture.",
|
||||
"invoice_need_report_title": "Rapport requis",
|
||||
"invoice_need_report_message": "Veuillez exécuter un rapport de temps avant d'essayer de créer une facture à partir de celui-ci.",
|
||||
"invoice_due_before_issue": "La date d'échéance ne peut pas être antérieure à la date d'émission.",
|
||||
"invoice_paid_before_issue": "La date de paiement ne peut pas être antérieure à la date d'émission.",
|
||||
"enable_invoicing_feature": "Activer la facturation (nécessite le journal de temps)",
|
||||
"invoice_company_profile": "Profil de l'entreprise",
|
||||
"invoice_company_name": "Nom de l'entreprise",
|
||||
"invoice_company_address": "Adresse",
|
||||
"invoice_company_phone": "Téléphone",
|
||||
"invoice_company_email": "E-mail",
|
||||
"invoice_company_tax_id": "Numéro fiscal",
|
||||
"invoice_company_payment_details": "Détails de paiement",
|
||||
"invoice_company_logo": "Logo",
|
||||
"invoice_company_logo_choose": "Choisir un logo",
|
||||
"invoice_company_logo_set": "Le logo a été défini",
|
||||
"invoice_company_logo_not_set": "Logo non défini",
|
||||
"invoice_number_unique": "Le numéro de facture doit être unique. Ce numéro de facture existe déjà.",
|
||||
"invoice_invalid_amount": "Le montant est invalide",
|
||||
"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_required": "Veuillez sélectionner une facture spécifique avant d'essayer de supprimer la facture."
|
||||
"day": "Jour"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ from PySide6.QtGui import (
|
|||
QGuiApplication,
|
||||
QKeySequence,
|
||||
QTextCursor,
|
||||
QTextListFormat,
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
|
|
@ -870,13 +871,6 @@ 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, 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.
|
||||
"""
|
||||
if not getattr(self.cfg, "move_todos", False):
|
||||
|
|
@ -891,9 +885,7 @@ 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+(.*)$")
|
||||
checked_re = re.compile(r"^(\s*)-\s*\[[xX☑]\]\s+(.*)$")
|
||||
fence_re = re.compile(r"^\s*(`{3,}|~{3,})")
|
||||
unchecked_re = re.compile(r"^\s*-\s*\[[\s☐]\]\s+")
|
||||
|
||||
def _normalize_heading(text: str) -> str:
|
||||
"""
|
||||
|
|
@ -904,47 +896,13 @@ class MainWindow(QMainWindow):
|
|||
text = re.sub(r"\s+#+\s*$", "", text)
|
||||
return text.strip()
|
||||
|
||||
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(
|
||||
def _insert_todos_under_heading(
|
||||
target_lines: list[str],
|
||||
heading_level: int,
|
||||
heading_text: str,
|
||||
blocks: list[list[str]],
|
||||
todos: list[str],
|
||||
) -> list[str]:
|
||||
"""Ensure a heading exists and append blocks to the end of its section."""
|
||||
"""Ensure a heading exists and append todos to the end of its section."""
|
||||
normalized = _normalize_heading(heading_text)
|
||||
|
||||
# 1) Find existing heading with same text (any level)
|
||||
|
|
@ -984,137 +942,15 @@ class MainWindow(QMainWindow):
|
|||
):
|
||||
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)
|
||||
for todo in todos:
|
||||
target_lines.insert(insert_at, todo)
|
||||
insert_at += 1
|
||||
|
||||
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)
|
||||
# Collect moved todos as (heading_info, item_text)
|
||||
# heading_info is either None or (level, heading_text)
|
||||
moved_blocks: list[tuple[tuple[int, str] | None, list[str]]] = []
|
||||
moved_items: list[tuple[tuple[int, str] | None, str]] = []
|
||||
any_moved = False
|
||||
|
||||
# Look back N days (yesterday = 1, up to `days_back`)
|
||||
|
|
@ -1130,15 +966,7 @@ class MainWindow(QMainWindow):
|
|||
moved_from_this_day = False
|
||||
current_heading: tuple[int, str] | None = None
|
||||
|
||||
in_fence = False
|
||||
fence_marker: str | None = None
|
||||
|
||||
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:
|
||||
for line in lines:
|
||||
# Track the last seen heading (# / ## / ###)
|
||||
m_head = heading_re.match(line)
|
||||
if m_head:
|
||||
|
|
@ -1146,71 +974,20 @@ class MainWindow(QMainWindow):
|
|||
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")
|
||||
# Keep headings in the original day
|
||||
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,
|
||||
# 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
|
||||
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))
|
||||
# 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
|
||||
|
||||
# We consumed the block; keep scanning from the checked checkbox (or EOF)
|
||||
continue
|
||||
|
||||
# Default: keep the line on the original day
|
||||
else:
|
||||
remaining_lines.append(line)
|
||||
in_fence, fence_marker = _update_fence_state(
|
||||
line, in_fence, fence_marker
|
||||
)
|
||||
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(
|
||||
|
|
@ -1222,52 +999,33 @@ class MainWindow(QMainWindow):
|
|||
if not any_moved:
|
||||
return False
|
||||
|
||||
# --- Merge all moved blocks into the *target* date ---
|
||||
# --- Merge all moved items into the *target* date ---
|
||||
|
||||
target_text = self.db.get_entry(target_iso) or ""
|
||||
# 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")
|
||||
target_lines = target_text.split("\n") if target_text else []
|
||||
|
||||
by_heading: dict[tuple[int, str], list[list[str]]] = {}
|
||||
plain_blocks: list[list[str]] = []
|
||||
by_heading: dict[tuple[int, str], list[str]] = {}
|
||||
plain_items: list[str] = []
|
||||
|
||||
for heading_info, block in moved_blocks:
|
||||
for heading_info, item_text in moved_items:
|
||||
todo_line = f"- [ ] {item_text}"
|
||||
if heading_info is None:
|
||||
plain_blocks.append(block)
|
||||
# No heading above this checkbox in the source: behave as before
|
||||
plain_items.append(todo_line)
|
||||
else:
|
||||
by_heading.setdefault(heading_info, []).append(block)
|
||||
by_heading.setdefault(heading_info, []).append(todo_line)
|
||||
|
||||
# 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
|
||||
# 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
|
||||
)
|
||||
|
||||
# Then append all blocks without headings at the end, like before
|
||||
if plain_blocks:
|
||||
# Then append all items without headings at the end, like before
|
||||
if plain_items:
|
||||
if target_lines and target_lines[-1].strip():
|
||||
target_lines.append("") # one blank line before the "unsectioned" todos
|
||||
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
|
||||
target_lines.extend(plain_items)
|
||||
|
||||
new_target_text = "\n".join(target_lines)
|
||||
if not new_target_text.endswith("\n"):
|
||||
|
|
@ -1483,58 +1241,46 @@ class MainWindow(QMainWindow):
|
|||
self._toolbar_bound = True
|
||||
|
||||
def _sync_toolbar(self):
|
||||
"""
|
||||
Keep the toolbar "sticky" by reflecting the markdown state at the current caret/selection.
|
||||
"""
|
||||
fmt = self.editor.currentCharFormat()
|
||||
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(bold_on)
|
||||
self.toolBar.actItalic.setChecked(italic_on)
|
||||
self.toolBar.actStrike.setChecked(strike_on)
|
||||
self.toolBar.actBold.setChecked(fmt.fontWeight() == QFont.Weight.Bold)
|
||||
self.toolBar.actItalic.setChecked(fmt.fontItalic())
|
||||
self.toolBar.actStrike.setChecked(fmt.fontStrikeOut())
|
||||
|
||||
# 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))
|
||||
# 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)
|
||||
|
||||
QSignalBlocker(self.toolBar.actH1)
|
||||
QSignalBlocker(self.toolBar.actH2)
|
||||
QSignalBlocker(self.toolBar.actH3)
|
||||
QSignalBlocker(self.toolBar.actNormal)
|
||||
|
||||
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))
|
||||
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))
|
||||
|
||||
# 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)
|
||||
QSignalBlocker(self.toolBar.actCheckboxes)
|
||||
|
||||
self.toolBar.actBullets.setChecked(bullets_on)
|
||||
self.toolBar.actNumbers.setChecked(numbers_on)
|
||||
self.toolBar.actCheckboxes.setChecked(checkboxes_on)
|
||||
self.toolBar.actBullets.setChecked(bool(bullets_on))
|
||||
self.toolBar.actNumbers.setChecked(bool(numbers_on))
|
||||
|
||||
def _change_font_size(self, delta: int) -> None:
|
||||
"""Change font size for all editor tabs and save the setting."""
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -111,25 +111,6 @@ class PomodoroManager:
|
|||
self._parent = parent_window
|
||||
self._active_timer: Optional[PomodoroTimer] = None
|
||||
|
||||
@staticmethod
|
||||
def _seconds_to_logged_hours(elapsed_seconds: int) -> float:
|
||||
"""Convert elapsed seconds to decimal hours for logging.
|
||||
|
||||
Rules:
|
||||
- For very short runs (< 15 minutes), always round up to 0.25h (15 minutes).
|
||||
- Otherwise, round to the closest 0.25h (15-minute) increment.
|
||||
Halfway cases (e.g., 22.5 minutes) round up.
|
||||
"""
|
||||
if elapsed_seconds < 0:
|
||||
elapsed_seconds = 0
|
||||
|
||||
# 15 minutes = 900 seconds
|
||||
if elapsed_seconds < 900:
|
||||
return 0.25
|
||||
|
||||
quarters = int(math.floor((elapsed_seconds / 900.0) + 0.5))
|
||||
return quarters * 0.25
|
||||
|
||||
def start_timer_for_line(self, line_text: str, date_iso: str):
|
||||
"""
|
||||
Start a new timer for the given line of text and embed it into the
|
||||
|
|
@ -175,8 +156,9 @@ class PomodoroManager:
|
|||
|
||||
def _on_timer_stopped(self, elapsed_seconds: int, task_text: str, date_iso: str):
|
||||
"""Handle timer stop - open time log dialog with pre-filled data."""
|
||||
# Convert seconds to decimal hours, and handle rounding up or down
|
||||
hours = self._seconds_to_logged_hours(elapsed_seconds)
|
||||
# Convert seconds to decimal hours, rounding up to the nearest 0.25 hour (15 minutes)
|
||||
quarter_hours = math.ceil(elapsed_seconds / 900)
|
||||
hours = quarter_hours * 0.25
|
||||
|
||||
# Ensure minimum of 0.25 hours
|
||||
if hours < 0.25:
|
||||
|
|
|
|||
|
|
@ -532,7 +532,7 @@ class SettingsDialog(QDialog):
|
|||
def _on_choose_logo(self) -> None:
|
||||
path, _ = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
strings._("invoice_company_logo_choose"),
|
||||
strings._("company_logo_choose"),
|
||||
"",
|
||||
"Images (*.png *.jpg *.jpeg *.bmp)",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1055,7 +1055,6 @@ class TimeReportDialog(QDialog):
|
|||
self.range_preset.addItem(strings._("today"), "today")
|
||||
self.range_preset.addItem(strings._("last_week"), "last_week")
|
||||
self.range_preset.addItem(strings._("this_week"), "this_week")
|
||||
self.range_preset.addItem(strings._("last_month"), "last_month")
|
||||
self.range_preset.addItem(strings._("this_month"), "this_month")
|
||||
self.range_preset.addItem(strings._("this_year"), "this_year")
|
||||
self.range_preset.currentIndexChanged.connect(self._on_range_preset_changed)
|
||||
|
|
@ -1215,12 +1214,6 @@ class TimeReportDialog(QDialog):
|
|||
start = start_of_this_week.addDays(-7) # last week's Monday
|
||||
end = start_of_this_week.addDays(-1) # last week's Sunday
|
||||
|
||||
elif preset == "last_month":
|
||||
# Previous calendar month (full month)
|
||||
start_of_this_month = QDate(today.year(), today.month(), 1)
|
||||
start = start_of_this_month.addMonths(-1)
|
||||
end = start_of_this_month.addDays(-1)
|
||||
|
||||
elif preset == "this_month":
|
||||
start = QDate(today.year(), today.month(), 1)
|
||||
end = today
|
||||
|
|
|
|||
|
|
@ -98,7 +98,6 @@ 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
|
||||
|
|
@ -127,14 +126,22 @@ class ToolBar(QToolBar):
|
|||
self.actDocuments = QAction("📁", self)
|
||||
self.actDocuments.setToolTip(strings._("toolbar_documents"))
|
||||
self.actDocuments.triggered.connect(self.documentsRequested)
|
||||
# Headings are mutually exclusive (like radio buttons)
|
||||
|
||||
# Set exclusive buttons in QActionGroups
|
||||
self.grpHeadings = QActionGroup(self)
|
||||
self.grpHeadings.setExclusive(True)
|
||||
for a in (self.actH1, self.actH2, self.actH3, self.actNormal):
|
||||
for a in (
|
||||
self.actBold,
|
||||
self.actItalic,
|
||||
self.actStrike,
|
||||
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):
|
||||
|
|
|
|||
|
|
@ -95,9 +95,6 @@ class VersionChecker:
|
|||
"""
|
||||
return self._parse_version(available) > self._parse_version(current)
|
||||
|
||||
def _running_in_appimage(self) -> bool:
|
||||
return "APPIMAGE" in os.environ
|
||||
|
||||
# ---------- Public entrypoint for Help → Version ---------- #
|
||||
|
||||
def show_version_dialog(self) -> None:
|
||||
|
|
@ -117,8 +114,8 @@ class VersionChecker:
|
|||
check_button = box.addButton(
|
||||
strings._("check_for_updates"), QMessageBox.ActionRole
|
||||
)
|
||||
|
||||
box.addButton(QMessageBox.Close)
|
||||
|
||||
box.exec()
|
||||
|
||||
if box.clickedButton() is check_button:
|
||||
|
|
@ -162,9 +159,6 @@ class VersionChecker:
|
|||
return
|
||||
|
||||
# Newer version is available
|
||||
|
||||
if self._running_in_appimage():
|
||||
# If running in an AppImage, offer to download the new AppImage
|
||||
reply = QMessageBox.question(
|
||||
self._parent,
|
||||
strings._("update"),
|
||||
|
|
@ -180,14 +174,6 @@ class VersionChecker:
|
|||
return
|
||||
|
||||
self._download_and_verify_appimage(available_raw)
|
||||
else:
|
||||
# If not running in an AppImage, just report that there's a new version.
|
||||
QMessageBox.information(
|
||||
self._parent,
|
||||
strings._("update"),
|
||||
(strings._("there_is_a_new_version_available") + available_raw),
|
||||
)
|
||||
return
|
||||
|
||||
# ---------- Download + verification helpers ---------- #
|
||||
def _download_file(
|
||||
|
|
|
|||
13
debian/bouquin.desktop
vendored
13
debian/bouquin.desktop
vendored
|
|
@ -1,13 +0,0 @@
|
|||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=Bouquin
|
||||
GenericName=Journal
|
||||
Comment=Daily planner with calendar, keyword searching, version control, time logging and document management
|
||||
Exec=bouquin
|
||||
Icon=bouquin
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Office;Calendar;
|
||||
Keywords=Journal;Diary;Notes;Notebook;
|
||||
StartupNotify=true
|
||||
X-GNOME-Gettext-Domain=bouquin
|
||||
2
debian/bouquin.install
vendored
2
debian/bouquin.install
vendored
|
|
@ -1,2 +0,0 @@
|
|||
debian/bouquin.desktop usr/share/applications/
|
||||
bouquin/icons/bouquin.svg usr/share/icons/hicolor/scalable/apps/
|
||||
34
debian/changelog
vendored
34
debian/changelog
vendored
|
|
@ -1,37 +1,3 @@
|
|||
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 <mig@mig5.net> 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
|
||||
|
||||
-- Miguel Jacq <mig@mig5.net> 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.
|
||||
* 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 <mig@mig5.net> Tue, 26 Dec 2025 18:00:00 +1100
|
||||
|
||||
bouquin (0.8.0) unstable; urgency=medium
|
||||
|
||||
* Add .desktop file for Debian
|
||||
* Fix Pomodoro timer rounding so it rounds up to 0.25, but rounds to closest quarter (up or down)
|
||||
* Allow setting a code block on a line that already has text (it will start a newline for the codeblock)
|
||||
* Retain indentation when tab is used to indent a line, unless enter is pressed twice or user deletes the indentation
|
||||
* Add ability to collapse/expand sections of text.
|
||||
* Add 'Last Month' date range for timesheet reports
|
||||
* Add missing strings (for English and French)
|
||||
* Don't offer to download latest AppImage unless we are running as an AppImage already
|
||||
|
||||
-- Miguel Jacq <mig@mig5.net> Tue, 23 Dec 2025 17:30:00 +1100
|
||||
|
||||
bouquin (0.7.5) unstable; urgency=medium
|
||||
|
||||
* Add libxcb-cursor0 dependency
|
||||
|
|
|
|||
305
poetry.lock
generated
305
poetry.lock
generated
|
|
@ -14,13 +14,13 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.1.4"
|
||||
version = "2025.11.12"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"},
|
||||
{file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"},
|
||||
{file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"},
|
||||
{file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -158,103 +158,103 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.13.2"
|
||||
version = "7.13.0"
|
||||
description = "Code coverage measurement for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{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"},
|
||||
{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"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
|
@ -321,28 +321,28 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "markdown"
|
||||
version = "3.10.1"
|
||||
version = "3.10"
|
||||
description = "Python implementation of John Gruber's Markdown."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "markdown-3.10.1-py3-none-any.whl", hash = "sha256:867d788939fe33e4b736426f5b9f651ad0c0ae0ecf89df0ca5d1176c70812fe3"},
|
||||
{file = "markdown-3.10.1.tar.gz", hash = "sha256:1c19c10bd5c14ac948c53d0d762a04e2fa35a6d58a6b7b1e6bfcbe6fefc0001a"},
|
||||
{file = "markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c"},
|
||||
{file = "markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e"},
|
||||
]
|
||||
|
||||
[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] (>=0.28.3)"]
|
||||
docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"]
|
||||
testing = ["coverage", "pyyaml"]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.0"
|
||||
version = "25.0"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"},
|
||||
{file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"},
|
||||
{file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
|
||||
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -560,58 +560,53 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.4.0"
|
||||
version = "2.3.0"
|
||||
description = "A lil' TOML parser"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{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"},
|
||||
{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"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -627,13 +622,13 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.6.3"
|
||||
version = "2.6.2"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"},
|
||||
{file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"},
|
||||
{file = "urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd"},
|
||||
{file = "urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
|
|
@ -645,4 +640,4 @@ zstd = ["backports-zstd (>=1.0.0)"]
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.10,<3.14"
|
||||
content-hash = "0ce61476b8c35f864d395ab3a4ee4645a1ad2e16aa800cba5e77e5bfee23d6a6"
|
||||
content-hash = "0241cd7378c45e79da728a23b89defa18f776ada9af1e60f2a19b0d90f3a2c19"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "bouquin"
|
||||
version = "0.8.3"
|
||||
version = "0.7.5"
|
||||
description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
|
||||
authors = ["Miguel Jacq <mig@mig5.net>"]
|
||||
readme = "README.md"
|
||||
|
|
@ -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.3"
|
||||
markdown = "^3.7"
|
||||
requests = "^2.32.5"
|
||||
markdown = "^3.10"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
bouquin = "bouquin.__main__:main"
|
||||
|
|
|
|||
48
release.sh
48
release.sh
|
|
@ -69,50 +69,4 @@ for dist in ${DISTS[@]}; do
|
|||
reprepro -b /home/user/git/repo includedeb "${release}" "${debfile}"
|
||||
done
|
||||
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
for dist in ${DISTS[@]}; do
|
||||
release=$(echo ${dist} | cut -d: -f2)
|
||||
REPO_RELEASE_ROOT="${REPO_ROOT}/${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} \
|
||||
.
|
||||
|
||||
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
|
||||
|
||||
echo "==> Syncing repo to server..."
|
||||
rsync -aHPvz --exclude=.git --delete "$REPO_ROOT/" "$REMOTE/"
|
||||
|
||||
echo "Done!"
|
||||
|
||||
ssh lupin.mig5.net "echo ${VERSION} | tee /var/www/bouquin/version.txt"
|
||||
ssh wolverine.mig5.net "echo ${VERSION} | tee /opt/www/mig5.net/bouquin/version.txt"
|
||||
|
|
|
|||
|
|
@ -1,95 +0,0 @@
|
|||
# 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.3
|
||||
|
||||
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
|
||||
* Fri Jan 30 2026 Miguel Jacq <mig@mig5.net> - %{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 <mig@mig5.net> - %{version}-%{release}
|
||||
- Add ability to delete an invoice via 'Manage Invoices' dialog
|
||||
* Fri Dec 26 2025 Miguel Jacq <mig@mig5.net> - %{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 <mig@mig5.net> - %{version}-%{release}
|
||||
- Initial RPM packaging for Fedora 42
|
||||
|
|
@ -3,8 +3,8 @@ from bouquin.code_block_editor_dialog import (
|
|||
CodeBlockEditorDialog,
|
||||
CodeEditorWithLineNumbers,
|
||||
)
|
||||
from PySide6.QtCore import QRect, QSize, Qt
|
||||
from PySide6.QtGui import QFont, QPaintEvent, QTextCursor
|
||||
from PySide6.QtCore import QRect, QSize
|
||||
from PySide6.QtGui import QFont, QPaintEvent
|
||||
from PySide6.QtWidgets import QPushButton
|
||||
|
||||
|
||||
|
|
@ -323,42 +323,3 @@ def test_code_editor_viewport_margins(qtbot, app):
|
|||
assert margins.top() == 0
|
||||
assert margins.right() == 0
|
||||
assert margins.bottom() == 0
|
||||
|
||||
|
||||
def test_code_editor_retains_indentation_on_enter(qtbot, app):
|
||||
"""Pressing Enter on an indented line retains indentation in code editor."""
|
||||
editor = CodeEditorWithLineNumbers()
|
||||
qtbot.addWidget(editor)
|
||||
editor.setPlainText("\tfoo")
|
||||
editor.show()
|
||||
|
||||
cursor = editor.textCursor()
|
||||
cursor.movePosition(QTextCursor.End)
|
||||
editor.setTextCursor(cursor)
|
||||
|
||||
qtbot.keyPress(editor, Qt.Key_Return)
|
||||
qtbot.wait(0)
|
||||
|
||||
assert editor.toPlainText().endswith("\tfoo\n\t")
|
||||
|
||||
|
||||
def test_code_editor_double_enter_on_empty_indent_resets(qtbot, app):
|
||||
"""Second Enter on an indentation-only line clears the indent in code editor."""
|
||||
editor = CodeEditorWithLineNumbers()
|
||||
qtbot.addWidget(editor)
|
||||
editor.setPlainText("\tfoo")
|
||||
editor.show()
|
||||
|
||||
cursor = editor.textCursor()
|
||||
cursor.movePosition(QTextCursor.End)
|
||||
editor.setTextCursor(cursor)
|
||||
|
||||
qtbot.keyPress(editor, Qt.Key_Return)
|
||||
qtbot.wait(0)
|
||||
assert editor.toPlainText().endswith("\tfoo\n\t")
|
||||
|
||||
qtbot.keyPress(editor, Qt.Key_Return)
|
||||
qtbot.wait(0)
|
||||
|
||||
assert editor.toPlainText().endswith("\tfoo\n\n")
|
||||
assert editor.textCursor().block().text() == ""
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
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
|
||||
|
|
@ -73,13 +72,8 @@ def test_apply_styles_and_headings(editor, qtbot):
|
|||
editor.apply_italic()
|
||||
editor.apply_strikethrough()
|
||||
editor.apply_heading(24)
|
||||
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
|
||||
md = editor.to_markdown()
|
||||
assert "**" in md and "*~~~~*" in md
|
||||
|
||||
|
||||
def test_toggle_lists_and_checkboxes(editor):
|
||||
|
|
@ -156,53 +150,6 @@ def test_enter_on_nonempty_list_continues(qtbot, editor):
|
|||
assert "\n\u2022 " in txt
|
||||
|
||||
|
||||
def test_tab_indentation_is_retained_on_newline(editor, qtbot):
|
||||
"""Pressing Enter on an indented line should retain the indentation."""
|
||||
qtbot.addWidget(editor)
|
||||
editor.show()
|
||||
editor.setPlainText("\tfoo")
|
||||
editor.moveCursor(QTextCursor.End)
|
||||
|
||||
qtbot.keyPress(editor, Qt.Key_Return)
|
||||
qtbot.wait(0)
|
||||
|
||||
assert editor.toPlainText().endswith("\tfoo\n\t")
|
||||
|
||||
|
||||
def test_double_enter_on_empty_indented_line_resets_indent(editor, qtbot):
|
||||
"""A second Enter on an indentation-only line should reset to column 0."""
|
||||
qtbot.addWidget(editor)
|
||||
editor.show()
|
||||
editor.setPlainText("\tfoo")
|
||||
editor.moveCursor(QTextCursor.End)
|
||||
|
||||
# First Enter inserts a new indented line
|
||||
qtbot.keyPress(editor, Qt.Key_Return)
|
||||
qtbot.wait(0)
|
||||
assert editor.toPlainText().endswith("\tfoo\n\t")
|
||||
|
||||
# Second Enter on the now-empty indented line removes the indent
|
||||
qtbot.keyPress(editor, Qt.Key_Return)
|
||||
qtbot.wait(0)
|
||||
|
||||
assert editor.toPlainText().endswith("\tfoo\n\n")
|
||||
# Cursor should be on a fresh unindented blank line
|
||||
assert editor.textCursor().block().text() == ""
|
||||
|
||||
|
||||
def test_nested_list_continuation_preserves_indentation(editor, qtbot):
|
||||
"""Enter on an indented bullet should keep indent + bullet prefix."""
|
||||
qtbot.addWidget(editor)
|
||||
editor.show()
|
||||
editor.from_markdown("\t- item")
|
||||
editor.moveCursor(QTextCursor.End)
|
||||
|
||||
qtbot.keyPress(editor, Qt.Key_Return)
|
||||
qtbot.wait(0)
|
||||
|
||||
assert "\n\t\u2022 " in editor.toPlainText()
|
||||
|
||||
|
||||
def test_enter_on_empty_list_marks_empty(qtbot, editor):
|
||||
qtbot.addWidget(editor)
|
||||
editor.show()
|
||||
|
|
@ -234,116 +181,6 @@ def test_triple_backtick_triggers_code_dialog_but_no_block_on_empty_code(editor,
|
|||
assert t == ""
|
||||
|
||||
|
||||
def _find_first_block(doc, predicate):
|
||||
b = doc.begin()
|
||||
while b.isValid():
|
||||
if predicate(b):
|
||||
return b
|
||||
b = b.next()
|
||||
return None
|
||||
|
||||
|
||||
def test_collapse_selection_wraps_and_hides_blocks(editor, qtbot):
|
||||
"""Collapsing a selection should insert header/end marker and hide content."""
|
||||
qtbot.addWidget(editor)
|
||||
editor.show()
|
||||
editor.setPlainText("a\nb\nc\n")
|
||||
doc = editor.document()
|
||||
|
||||
# Select lines b..c
|
||||
b_block = doc.findBlockByNumber(1)
|
||||
c_block = doc.findBlockByNumber(2)
|
||||
cur = editor.textCursor()
|
||||
cur.setPosition(b_block.position())
|
||||
cur.setPosition(c_block.position() + c_block.length() - 1, QTextCursor.KeepAnchor)
|
||||
editor.setTextCursor(cur)
|
||||
|
||||
editor.collapse_selection()
|
||||
qtbot.wait(0)
|
||||
|
||||
# Header and end marker should exist as their own blocks
|
||||
header = _find_first_block(doc, lambda bl: bl.text().lstrip().startswith("▸"))
|
||||
assert header is not None
|
||||
assert "▸" in header.text() and "expand" in header.text()
|
||||
|
||||
end_marker = _find_first_block(doc, lambda bl: "bouquin:collapse:end" in bl.text())
|
||||
assert end_marker is not None
|
||||
|
||||
# Inner blocks should be hidden; end marker always hidden
|
||||
inner1 = header.next()
|
||||
inner2 = inner1.next()
|
||||
assert inner1.text() == "b"
|
||||
assert inner2.text() == "c"
|
||||
assert inner1.isVisible() is False
|
||||
assert inner2.isVisible() is False
|
||||
assert end_marker.isVisible() is False
|
||||
|
||||
|
||||
def test_toggle_collapse_expands_and_updates_header(editor, qtbot):
|
||||
"""Toggling a collapse header should reveal hidden blocks and flip label."""
|
||||
qtbot.addWidget(editor)
|
||||
editor.show()
|
||||
editor.setPlainText("a\nb\nc\n")
|
||||
doc = editor.document()
|
||||
|
||||
# Select b..c and collapse
|
||||
b_block = doc.findBlockByNumber(1)
|
||||
c_block = doc.findBlockByNumber(2)
|
||||
cur = editor.textCursor()
|
||||
cur.setPosition(b_block.position(), QTextCursor.MoveMode.MoveAnchor)
|
||||
cur.setPosition(
|
||||
c_block.position() + c_block.length() - 1, QTextCursor.MoveMode.KeepAnchor
|
||||
)
|
||||
editor.setTextCursor(cur)
|
||||
editor.collapse_selection()
|
||||
qtbot.wait(0)
|
||||
|
||||
header = _find_first_block(doc, lambda bl: bl.text().lstrip().startswith("▸"))
|
||||
assert header is not None
|
||||
|
||||
# Toggle to expand
|
||||
editor._toggle_collapse_at_block(header)
|
||||
qtbot.wait(0)
|
||||
|
||||
header2 = doc.findBlock(header.position())
|
||||
assert "▾" in header2.text() and "collapse" in header2.text()
|
||||
assert header2.next().isVisible() is True
|
||||
assert header2.next().next().isVisible() is True
|
||||
|
||||
|
||||
def test_collapse_selection_without_trailing_newline_keeps_marker_on_own_line(
|
||||
editor, qtbot
|
||||
):
|
||||
"""Selections reaching EOF without a trailing newline should still fold correctly."""
|
||||
qtbot.addWidget(editor)
|
||||
editor.show()
|
||||
editor.setPlainText("a\nb\nc") # no trailing newline
|
||||
doc = editor.document()
|
||||
|
||||
# Bottom-up selection of last two lines (c..b)
|
||||
b_block = doc.findBlockByNumber(1)
|
||||
c_block = doc.findBlockByNumber(2)
|
||||
cur = editor.textCursor()
|
||||
cur.setPosition(c_block.position() + len(c_block.text()))
|
||||
cur.setPosition(b_block.position(), QTextCursor.KeepAnchor)
|
||||
editor.setTextCursor(cur)
|
||||
|
||||
editor.collapse_selection()
|
||||
qtbot.wait(0)
|
||||
|
||||
end_marker = _find_first_block(doc, lambda bl: "bouquin:collapse:end" in bl.text())
|
||||
assert end_marker is not None
|
||||
|
||||
# End marker is its own block, and remains hidden
|
||||
assert end_marker.text().strip() == "<!-- bouquin:collapse:end -->"
|
||||
assert end_marker.isVisible() is False
|
||||
|
||||
# The last content line should be hidden (folded)
|
||||
header = _find_first_block(doc, lambda bl: bl.text().lstrip().startswith("▸"))
|
||||
assert header is not None
|
||||
assert header.next().isVisible() is False
|
||||
|
||||
|
||||
def test_down_escapes_from_last_code_line(editor, qtbot):
|
||||
editor.from_markdown("```\nLINE\n```\n")
|
||||
# Put caret at end of "LINE"
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@ def test_pomodoro_manager_on_timer_stopped_minimum_hours(
|
|||
|
||||
|
||||
def test_pomodoro_manager_on_timer_stopped_rounding(qtbot, app, fresh_db, monkeypatch):
|
||||
"""Elapsed time should be rounded to a 0.25-hour increment."""
|
||||
"""Elapsed time should be rounded up to the nearest 0.25 hours."""
|
||||
parent = DummyMainWindow(app)
|
||||
qtbot.addWidget(parent)
|
||||
qtbot.addWidget(parent.time_log)
|
||||
|
|
@ -300,31 +300,6 @@ def test_pomodoro_manager_on_timer_stopped_rounding(qtbot, app, fresh_db, monkey
|
|||
assert hours_set * 4 == int(hours_set * 4)
|
||||
|
||||
|
||||
def test_seconds_to_logged_hours_nearest_quarter_rounding():
|
||||
"""Seconds -> hours uses nearest-quarter rounding with a 15-min minimum."""
|
||||
# Import the pure conversion helper directly (no Qt required)
|
||||
from bouquin.pomodoro_timer import PomodoroManager
|
||||
|
||||
# <15 minutes always rounds up to 0.25
|
||||
assert PomodoroManager._seconds_to_logged_hours(1) == 0.25
|
||||
assert PomodoroManager._seconds_to_logged_hours(899) == 0.25
|
||||
|
||||
# 15 minutes exact
|
||||
assert PomodoroManager._seconds_to_logged_hours(900) == 0.25
|
||||
|
||||
# Examples from the spec: closest quarter-hour
|
||||
# 33 minutes -> closer to 0.50 than 0.75
|
||||
assert PomodoroManager._seconds_to_logged_hours(33 * 60) == 0.50
|
||||
# 40 minutes -> closer to 0.75 than 0.50
|
||||
assert PomodoroManager._seconds_to_logged_hours(40 * 60) == 0.75
|
||||
|
||||
# Halfway case: 22.5 min is exactly between 0.25 and 0.50 -> round up
|
||||
assert PomodoroManager._seconds_to_logged_hours(int(22.5 * 60)) == 0.50
|
||||
|
||||
# Sanity: 1 hour stays 1.0
|
||||
assert PomodoroManager._seconds_to_logged_hours(60 * 60) == 1.00
|
||||
|
||||
|
||||
def test_pomodoro_manager_on_timer_stopped_prefills_note(
|
||||
qtbot, app, fresh_db, monkeypatch
|
||||
):
|
||||
|
|
|
|||
|
|
@ -1211,26 +1211,6 @@ def test_time_report_dialog_default_date_range(qtbot, fresh_db):
|
|||
assert dialog.to_date.date() == today
|
||||
|
||||
|
||||
def test_time_report_dialog_last_month_preset_sets_full_previous_month(qtbot, fresh_db):
|
||||
"""Selecting 'Last month' sets the date range to the previous calendar month."""
|
||||
strings.load_strings("en")
|
||||
dialog = TimeReportDialog(fresh_db)
|
||||
qtbot.addWidget(dialog)
|
||||
|
||||
idx = dialog.range_preset.findData("last_month")
|
||||
assert idx != -1
|
||||
|
||||
today = QDate.currentDate()
|
||||
start_of_this_month = QDate(today.year(), today.month(), 1)
|
||||
expected_start = start_of_this_month.addMonths(-1)
|
||||
expected_end = start_of_this_month.addDays(-1)
|
||||
|
||||
dialog.range_preset.setCurrentIndex(idx)
|
||||
|
||||
assert dialog.from_date.date() == expected_start
|
||||
assert dialog.to_date.date() == expected_end
|
||||
|
||||
|
||||
def test_time_report_dialog_run_report(qtbot, fresh_db):
|
||||
"""Run a time report."""
|
||||
strings.load_strings("en")
|
||||
|
|
|
|||
|
|
@ -173,6 +173,45 @@ def test_check_for_updates_already_latest(qtbot, app):
|
|||
assert mock_info.called
|
||||
|
||||
|
||||
def test_check_for_updates_new_version_available_declined(qtbot, app):
|
||||
"""Test check for updates when new version is available but user declines."""
|
||||
parent = QWidget()
|
||||
qtbot.addWidget(parent)
|
||||
checker = VersionChecker(parent)
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.text = "2.0.0"
|
||||
mock_response.raise_for_status = Mock()
|
||||
|
||||
with patch("requests.get", return_value=mock_response):
|
||||
with patch("importlib.metadata.version", return_value="1.0.0"):
|
||||
with patch.object(QMessageBox, "question", return_value=QMessageBox.No):
|
||||
# Should not proceed to download
|
||||
checker.check_for_updates()
|
||||
|
||||
|
||||
def test_check_for_updates_new_version_available_accepted(qtbot, app):
|
||||
"""Test check for updates when new version is available and user accepts."""
|
||||
parent = QWidget()
|
||||
qtbot.addWidget(parent)
|
||||
checker = VersionChecker(parent)
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.text = "2.0.0"
|
||||
mock_response.raise_for_status = Mock()
|
||||
|
||||
with patch("requests.get", return_value=mock_response):
|
||||
with patch("importlib.metadata.version", return_value="1.0.0"):
|
||||
with patch.object(QMessageBox, "question", return_value=QMessageBox.Yes):
|
||||
with patch.object(
|
||||
checker, "_download_and_verify_appimage"
|
||||
) as mock_download:
|
||||
checker.check_for_updates()
|
||||
|
||||
# Should call download
|
||||
mock_download.assert_called_once_with("2.0.0")
|
||||
|
||||
|
||||
def test_download_file_success(qtbot, app, tmp_path):
|
||||
"""Test downloading a file successfully."""
|
||||
checker = VersionChecker()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue