Add documents feature
Some checks failed
CI / test (push) Failing after 3m53s
Lint / test (push) Successful in 33s
Trivy / test (push) Successful in 23s

This commit is contained in:
Miguel Jacq 2025-12-01 15:51:47 +11:00
parent 23b6ce62a3
commit 422411f12e
Signed by: mig5
GPG key ID: 59B3F0C24135C6A9
18 changed files with 1521 additions and 216 deletions

View file

@ -1,3 +1,10 @@
# 0.6.0
* Add 'Documents' feature. Documents are tied to Projects in the same way as Time Logging, and can be tagged via the Tags feature.
* Close time log dialog if opened via the + button from sidebar widget
* Only show tags in Statistics widget if tags are enabled
* Fix rounding up/down in Pomodoro timer to the closest 15 min interval
# 0.5.5
* Add + button to time log widget in side bar to have a simplified log entry dialog (without summary or report option)

View file

@ -72,6 +72,7 @@ report from within the app, or optionally to check for new versions to upgrade t
* English, French and Italian locales provided
* 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.
## How to install

View file

@ -6,11 +6,13 @@ import hashlib
import html
import json
import markdown
import mimetypes
import re
from dataclasses import dataclass
from pathlib import Path
from sqlcipher3 import dbapi2 as sqlite
from sqlcipher3 import Binary
from typing import List, Sequence, Tuple, Dict
@ -30,6 +32,15 @@ TimeLogRow = Tuple[
int, # minutes
str | None, # note
]
DocumentRow = Tuple[
int, # id
int, # project_id
str, # project_name
str, # file_name
str | None, # description
int, # size_bytes
str, # uploaded_at (ISO)
]
_TAG_COLORS = [
"#FFB3BA", # soft red
@ -65,6 +76,7 @@ class DBConfig:
tags: bool = True
time_log: bool = True
reminders: bool = True
documents: bool = True
locale: str = "en"
font_size: int = 11
@ -211,6 +223,35 @@ class DBManager:
CREATE INDEX IF NOT EXISTS ix_reminders_active
ON reminders(active);
CREATE TABLE IF NOT EXISTS project_documents (
id INTEGER PRIMARY KEY,
project_id INTEGER NOT NULL, -- FK to projects.id
file_name TEXT NOT NULL, -- original filename
mime_type TEXT, -- optional
description TEXT,
size_bytes INTEGER NOT NULL,
uploaded_at TEXT NOT NULL DEFAULT (
strftime('%Y-%m-%d','now')
),
data BLOB NOT NULL,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE RESTRICT
);
CREATE INDEX IF NOT EXISTS ix_project_documents_project
ON project_documents(project_id);
-- New: tags attached to documents (like page_tags, but for docs)
CREATE TABLE IF NOT EXISTS document_tags (
document_id INTEGER NOT NULL, -- FK to project_documents.id
tag_id INTEGER NOT NULL, -- FK to tags.id
PRIMARY KEY (document_id, tag_id),
FOREIGN KEY(document_id) REFERENCES project_documents(id) ON DELETE CASCADE,
FOREIGN KEY(tag_id) REFERENCES tags(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS ix_document_tags_tag_id
ON document_tags(tag_id);
"""
)
self.conn.commit()
@ -248,18 +289,30 @@ class DBManager:
).fetchone()
return row[0] if row else ""
def search_entries(self, text: str) -> list[str]:
def search_entries(self, text: str) -> list[tuple[str, str, str, str, str | None]]:
"""
Search for entries by term or tag name.
This only works against the latest version of the page.
Returns both pages and documents.
kind = "page" or "document"
key = date_iso (page) or str(doc_id) (document)
title = heading for the result ("YYYY-MM-DD" or "Document")
text = source text for the snippet
aux = extra info (file_name for documents, else None)
"""
cur = self.conn.cursor()
q = text.strip()
if not q:
return []
pattern = f"%{q.lower()}%"
rows = cur.execute(
results: list[tuple[str, str, str, str, str | None]] = []
# --- Pages: content or tag matches ---------------------------------
page_rows = cur.execute(
"""
SELECT DISTINCT p.date, v.content
SELECT DISTINCT p.date AS date_iso, v.content
FROM pages AS p
JOIN versions AS v
ON v.id = p.current_version_id
@ -276,7 +329,54 @@ class DBManager:
""",
(pattern, pattern),
).fetchall()
return [(r[0], r[1]) for r in rows]
for r in page_rows:
date_iso = r["date_iso"]
content = r["content"]
results.append(("page", date_iso, date_iso, content, None))
# --- Documents: file name, description, or tag matches -------------
doc_rows = cur.execute(
"""
SELECT DISTINCT
d.id AS doc_id,
d.file_name AS file_name,
d.uploaded_at AS uploaded_at,
COALESCE(d.description, '') AS description,
COALESCE(t.name, '') AS tag_name
FROM project_documents AS d
LEFT JOIN document_tags AS dt
ON dt.document_id = d.id
LEFT JOIN tags AS t
ON t.id = dt.tag_id
WHERE
LOWER(d.file_name) LIKE ?
OR LOWER(COALESCE(d.description, '')) LIKE ?
OR LOWER(COALESCE(t.name, '')) LIKE ?
ORDER BY LOWER(d.file_name);
""",
(pattern, pattern, pattern),
).fetchall()
for r in doc_rows:
doc_id = r["doc_id"]
file_name = r["file_name"]
description = r["description"] or ""
uploaded_at = r["uploaded_at"]
# Simple snippet source: file name + description
text_src = f"{file_name}\n{description}".strip()
results.append(
(
"document",
str(doc_id),
strings._("search_result_heading_document") + f" ({uploaded_at})",
text_src,
file_name,
)
)
return results
def dates_with_content(self) -> list[str]:
"""
@ -691,11 +791,12 @@ class DBManager:
def delete_tag(self, tag_id: int) -> None:
"""
Delete a tag entirely (removes it from all pages).
Delete a tag entirely (removes it from all pages and documents).
"""
with self.conn:
cur = self.conn.cursor()
cur.execute("DELETE FROM page_tags WHERE tag_id=?;", (tag_id,))
cur.execute("DELETE FROM document_tags WHERE tag_id=?;", (tag_id,))
cur.execute("DELETE FROM tags WHERE id=?;", (tag_id,))
def get_pages_for_tag(self, tag_name: str) -> list[Entry]:
@ -1137,3 +1238,341 @@ class DBManager:
cur = self.conn.cursor()
cur.execute("DELETE FROM reminders WHERE id = ?", (reminder_id,))
self.conn.commit()
# ------------------------- Documents logic here ------------------------#
def documents_for_project(self, project_id: int) -> list[DocumentRow]:
"""
Return metadata for all documents attached to a given project.
"""
cur = self.conn.cursor()
rows = cur.execute(
"""
SELECT
d.id,
d.project_id,
p.name AS project_name,
d.file_name,
d.description,
d.size_bytes,
d.uploaded_at
FROM project_documents AS d
JOIN projects AS p ON p.id = d.project_id
WHERE d.project_id = ?
ORDER BY d.uploaded_at DESC, LOWER(d.file_name);
""",
(project_id,),
).fetchall()
result: list[DocumentRow] = []
for r in rows:
result.append(
(
r["id"],
r["project_id"],
r["project_name"],
r["file_name"],
r["description"],
r["size_bytes"],
r["uploaded_at"],
)
)
return result
def search_documents(self, query: str) -> list[DocumentRow]:
"""Search documents across all projects.
The search is case-insensitive and matches against:
- file name
- description
- project name
- tag names associated with the document
"""
pattern = f"%{query.lower()}%"
cur = self.conn.cursor()
rows = cur.execute(
"""
SELECT DISTINCT
d.id,
d.project_id,
p.name AS project_name,
d.file_name,
d.description,
d.size_bytes,
d.uploaded_at
FROM project_documents AS d
LEFT JOIN projects AS p ON p.id = d.project_id
LEFT JOIN document_tags AS dt ON dt.document_id = d.id
LEFT JOIN tags AS t ON t.id = dt.tag_id
WHERE LOWER(d.file_name) LIKE :pat
OR LOWER(COALESCE(d.description, '')) LIKE :pat
OR LOWER(COALESCE(p.name, '')) LIKE :pat
OR LOWER(COALESCE(t.name, '')) LIKE :pat
ORDER BY d.uploaded_at DESC, LOWER(d.file_name);
""",
{"pat": pattern},
).fetchall()
result: list[DocumentRow] = []
for r in rows:
result.append(
(
r["id"],
r["project_id"],
r["project_name"],
r["file_name"],
r["description"],
r["size_bytes"],
r["uploaded_at"],
)
)
return result
def add_document_from_path(
self,
project_id: int,
file_path: str,
description: str | None = None,
) -> int:
"""
Read a file from disk and store it as a BLOB in project_documents.
"""
path = Path(file_path)
if not path.is_file():
raise ValueError(f"File does not exist: {file_path}")
data = path.read_bytes()
size_bytes = len(data)
file_name = path.name
mime_type, _ = mimetypes.guess_type(str(path))
mime_type = mime_type or None
with self.conn:
cur = self.conn.cursor()
cur.execute(
"""
INSERT INTO project_documents
(project_id, file_name, mime_type,
description, size_bytes, data)
VALUES (?, ?, ?, ?, ?, ?);
""",
(
project_id,
file_name,
mime_type,
description,
size_bytes,
Binary(data),
),
)
doc_id = cur.lastrowid or 0
return int(doc_id)
def update_document_description(self, doc_id: int, description: str | None) -> None:
with self.conn:
self.conn.execute(
"UPDATE project_documents SET description = ? WHERE id = ?;",
(description, doc_id),
)
def delete_document(self, doc_id: int) -> None:
with self.conn:
self.conn.execute("DELETE FROM project_documents WHERE id = ?;", (doc_id,))
def document_data(self, doc_id: int) -> bytes:
"""
Return just the raw bytes for a document.
"""
cur = self.conn.cursor()
row = cur.execute(
"SELECT data FROM project_documents WHERE id = ?;",
(doc_id,),
).fetchone()
if row is None:
raise KeyError(f"Unknown document id {doc_id}")
return bytes(row["data"])
def get_tags_for_document(self, document_id: int) -> list[TagRow]:
"""
Return (id, name, color) for all tags attached to this document.
"""
cur = self.conn.cursor()
rows = cur.execute(
"""
SELECT t.id, t.name, t.color
FROM document_tags dt
JOIN tags t ON t.id = dt.tag_id
WHERE dt.document_id = ?
ORDER BY LOWER(t.name);
""",
(document_id,),
).fetchall()
return [(r[0], r[1], r[2]) for r in rows]
def set_tags_for_document(self, document_id: int, tag_names: Sequence[str]) -> None:
"""
Replace the tag set for a document with the given names.
Behaviour mirrors set_tags_for_page.
"""
# Normalise + dedupe (case-insensitive)
clean_names: list[str] = []
seen: set[str] = set()
for name in tag_names:
name = name.strip()
if not name:
continue
key = name.lower()
if key in seen:
continue
seen.add(key)
clean_names.append(name)
with self.conn:
cur = self.conn.cursor()
# Ensure the document exists
exists = cur.execute(
"SELECT 1 FROM project_documents WHERE id = ?;", (document_id,)
).fetchone()
if not exists:
raise sqlite.IntegrityError(f"Unknown document id {document_id}")
if not clean_names:
cur.execute(
"DELETE FROM document_tags WHERE document_id = ?;",
(document_id,),
)
return
# For each tag name, reuse existing tag (case-insensitive) or create new
final_tag_names: list[str] = []
for name in clean_names:
existing = cur.execute(
"SELECT name FROM tags WHERE LOWER(name) = LOWER(?);", (name,)
).fetchone()
if existing:
final_tag_names.append(existing["name"])
else:
cur.execute(
"""
INSERT OR IGNORE INTO tags(name, color)
VALUES (?, ?);
""",
(name, self._default_tag_colour(name)),
)
final_tag_names.append(name)
# Lookup ids for the final tag names
placeholders = ",".join("?" for _ in final_tag_names)
rows = cur.execute(
f"""
SELECT id, name
FROM tags
WHERE name IN ({placeholders});
""", # nosec
tuple(final_tag_names),
).fetchall()
ids_by_name = {r["name"]: r["id"] for r in rows}
# Reset document_tags for this document
cur.execute(
"DELETE FROM document_tags WHERE document_id = ?;",
(document_id,),
)
for name in final_tag_names:
tag_id = ids_by_name.get(name)
if tag_id is not None:
cur.execute(
"""
INSERT OR IGNORE INTO document_tags(document_id, tag_id)
VALUES (?, ?);
""",
(document_id, tag_id),
)
def documents_by_date(self) -> Dict[_dt.date, int]:
"""
Return a mapping of date -> number of documents uploaded on that date.
The keys are datetime.date objects derived from the
project_documents.uploaded_at column, which is stored as a
YYYY-MM-DD ISO date string (or a timestamp whose leading part
is that date).
"""
cur = self.conn.cursor()
try:
rows = cur.execute(
"""
SELECT uploaded_at AS date_iso,
COUNT(*) AS c
FROM project_documents
WHERE uploaded_at IS NOT NULL
AND uploaded_at != ''
GROUP BY uploaded_at
ORDER BY uploaded_at;
"""
).fetchall()
except Exception:
# Older DBs without project_documents/uploaded_at → no document stats
return {}
result: Dict[_dt.date, int] = {}
for r in rows:
date_iso = r["date_iso"]
if not date_iso:
continue
# If uploaded_at ever contains a full timestamp, only use
# the leading date portion.
date_part = str(date_iso).split(" ", 1)[0][:10]
try:
d = _dt.date.fromisoformat(date_part)
except Exception: # nosec B112
continue
result[d] = int(r["c"])
return result
def todays_documents(self, date_iso: str) -> list[tuple[int, str, str | None, str]]:
"""
Return today's documents as
(doc_id, file_name, project_name, uploaded_at).
"""
cur = self.conn.cursor()
rows = cur.execute(
"""
SELECT d.id AS doc_id,
d.file_name AS file_name,
p.name AS project_name
FROM project_documents AS d
LEFT JOIN projects AS p ON p.id = d.project_id
WHERE d.uploaded_at LIKE ?
ORDER BY d.uploaded_at DESC, LOWER(d.file_name);
""",
(f"%{date_iso}%",),
).fetchall()
return [(r["doc_id"], r["file_name"], r["project_name"]) for r in rows]
def get_documents_for_tag(self, tag_name: str) -> list[tuple[int, str, str]]:
"""
Return (document_id, project_name, file_name) for documents with a given tag.
"""
cur = self.conn.cursor()
rows = cur.execute(
"""
SELECT d.id AS doc_id,
p.name AS project_name,
d.file_name
FROM project_documents AS d
JOIN document_tags AS dt ON dt.document_id = d.id
JOIN tags AS t ON t.id = dt.tag_id
LEFT JOIN projects AS p ON p.id = d.project_id
WHERE LOWER(t.name) = LOWER(?)
ORDER BY LOWER(d.file_name);
""",
(tag_name,),
).fetchall()
return [(r["doc_id"], r["project_name"], r["file_name"]) for r in rows]

594
bouquin/documents.py Normal file
View file

@ -0,0 +1,594 @@
from __future__ import annotations
from pathlib import Path
import tempfile
from typing import Optional
from PySide6.QtCore import Qt, QUrl
from PySide6.QtGui import QDesktopServices, QColor
from PySide6.QtWidgets import (
QDialog,
QVBoxLayout,
QHBoxLayout,
QFormLayout,
QComboBox,
QLineEdit,
QTableWidget,
QTableWidgetItem,
QAbstractItemView,
QHeaderView,
QPushButton,
QFileDialog,
QMessageBox,
QWidget,
QFrame,
QToolButton,
QListWidget,
QListWidgetItem,
QSizePolicy,
QStyle,
)
from .db import DBManager, DocumentRow
from .settings import load_db_config
from .time_log import TimeCodeManagerDialog
from . import strings
class TodaysDocumentsWidget(QFrame):
"""
Collapsible sidebar widget showing today's documents.
"""
def __init__(
self, db: DBManager, date_iso: str, parent: QWidget | None = None
) -> None:
super().__init__(parent)
self._db = db
self._current_date = date_iso
self.setFrameShape(QFrame.StyledPanel)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
# Header (toggle + open-documents button)
self.toggle_btn = QToolButton()
self.toggle_btn.setText(strings._("todays_documents"))
self.toggle_btn.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
self.toggle_btn.setCheckable(True)
self.toggle_btn.setChecked(False)
self.toggle_btn.setArrowType(Qt.RightArrow)
self.toggle_btn.clicked.connect(self._on_toggle)
self.open_btn = QToolButton()
self.open_btn.setIcon(
self.style().standardIcon(QStyle.SP_FileDialogDetailedView)
)
self.open_btn.setToolTip(strings._("project_documents_title"))
self.open_btn.setAutoRaise(True)
self.open_btn.clicked.connect(self._open_documents_dialog)
header = QHBoxLayout()
header.setContentsMargins(0, 0, 0, 0)
header.addWidget(self.toggle_btn)
header.addStretch(1)
header.addWidget(self.open_btn)
# Body: list of today's documents
self.body = QWidget()
body_layout = QVBoxLayout(self.body)
body_layout.setContentsMargins(0, 4, 0, 0)
body_layout.setSpacing(2)
self.list = QListWidget()
self.list.setSelectionMode(QAbstractItemView.SingleSelection)
self.list.setMaximumHeight(160)
self.list.itemDoubleClicked.connect(self._open_selected_document)
body_layout.addWidget(self.list)
self.body.setVisible(False)
main = QVBoxLayout(self)
main.setContentsMargins(0, 0, 0, 0)
main.addLayout(header)
main.addWidget(self.body)
# Initial fill
self.reload()
# ----- public API ---------------------------------------------------
def reload(self) -> None:
"""Refresh the list of today's documents."""
self.list.clear()
rows = self._db.todays_documents(self._current_date)
if not rows:
item = QListWidgetItem(strings._("todays_documents_none"))
item.setFlags(item.flags() & ~Qt.ItemIsEnabled)
self.list.addItem(item)
return
for doc_id, file_name, project_name in rows:
label = file_name
extra_parts = []
if project_name:
extra_parts.append(project_name)
if extra_parts:
label = f"{file_name} " + " · ".join(extra_parts)
item = QListWidgetItem(label)
item.setData(
Qt.ItemDataRole.UserRole,
{"doc_id": doc_id, "file_name": file_name},
)
self.list.addItem(item)
# ----- internals ----------------------------------------------------
def set_current_date(self, date_iso: str) -> None:
self._current_date = date_iso
self.reload()
def _on_toggle(self, checked: bool) -> None:
self.body.setVisible(checked)
self.toggle_btn.setArrowType(Qt.DownArrow if checked else Qt.RightArrow)
if checked:
self.reload()
def _open_selected_document(self, item: QListWidgetItem) -> None:
data = item.data(Qt.ItemDataRole.UserRole)
if not isinstance(data, dict):
return
doc_id = data.get("doc_id")
file_name = data.get("file_name") or ""
if doc_id is None or not file_name:
return
self._open_document(int(doc_id), file_name)
def _open_document(self, doc_id: int, file_name: str) -> None:
"""Open a document from the list."""
try:
data = self._db.document_data(doc_id)
except Exception as e:
QMessageBox.warning(
self,
strings._("project_documents_title"),
strings._("documents_open_failed").format(error=str(e)),
)
return
suffix = Path(file_name).suffix or ""
tmp = tempfile.NamedTemporaryFile(
prefix="bouquin_doc_",
suffix=suffix,
delete=False,
)
try:
tmp.write(data)
tmp.flush()
finally:
tmp.close()
QDesktopServices.openUrl(QUrl.fromLocalFile(tmp.name))
def _open_documents_dialog(self) -> None:
"""Open the full DocumentsDialog."""
dlg = DocumentsDialog(self._db, self)
dlg.exec()
# Refresh after any changes
self.reload()
class DocumentsDialog(QDialog):
"""
Per-project document manager.
- Choose a project
- See list of attached documents
- Add (from file), open (via temp file), delete
- Inline-edit description
- Inline-edit tags (comma-separated), using the global tags table
"""
FILE_COL = 0
TAGS_COL = 1
DESC_COL = 2
ADDED_COL = 3
SIZE_COL = 4
def __init__(
self,
db: DBManager,
parent: QWidget | None = None,
initial_project_id: Optional[int] = None,
) -> None:
super().__init__(parent)
self._db = db
self.cfg = load_db_config()
self._reloading_docs = False
self._search_text: str = ""
self.setWindowTitle(strings._("project_documents_title"))
self.resize(900, 450)
root = QVBoxLayout(self)
# --- Project selector -------------------------------------------------
form = QFormLayout()
proj_row = QHBoxLayout()
self.project_combo = QComboBox()
self.manage_projects_btn = QPushButton(strings._("manage_projects"))
self.manage_projects_btn.clicked.connect(self._manage_projects)
proj_row.addWidget(self.project_combo, 1)
proj_row.addWidget(self.manage_projects_btn)
form.addRow(strings._("project"), proj_row)
# --- Search box (all projects) ----------------------------------------
self.search_edit = QLineEdit()
self.search_edit.setClearButtonEnabled(True)
self.search_edit.setPlaceholderText(strings._("documents_search_placeholder"))
self.search_edit.textChanged.connect(self._on_search_text_changed)
form.addRow(strings._("documents_search_label"), self.search_edit)
root.addLayout(form)
self.project_combo.currentIndexChanged.connect(self._on_project_changed)
# --- Table of documents ----------------------------------------------
self.table = QTableWidget()
self.table.setColumnCount(5)
self.table.setHorizontalHeaderLabels(
[
strings._("documents_col_file"), # FILE_COL
strings._("documents_col_tags"), # TAGS_COL
strings._("documents_col_description"), # DESC_COL
strings._("documents_col_added"), # ADDED_COL
strings._("documents_col_size"), # SIZE_COL
]
)
header = self.table.horizontalHeader()
header.setSectionResizeMode(self.FILE_COL, QHeaderView.Stretch)
header.setSectionResizeMode(self.TAGS_COL, QHeaderView.ResizeToContents)
header.setSectionResizeMode(self.DESC_COL, QHeaderView.Stretch)
header.setSectionResizeMode(self.ADDED_COL, QHeaderView.ResizeToContents)
header.setSectionResizeMode(self.SIZE_COL, QHeaderView.ResizeToContents)
self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.table.setSelectionMode(QAbstractItemView.SingleSelection)
# Editable: tags + description
self.table.setEditTriggers(
QAbstractItemView.DoubleClicked | QAbstractItemView.SelectedClicked
)
self.table.itemChanged.connect(self._on_item_changed)
self.table.itemDoubleClicked.connect(self._on_open_clicked)
root.addWidget(self.table, 1)
# --- Buttons ---------------------------------------------------------
btn_row = QHBoxLayout()
btn_row.addStretch(1)
self.add_btn = QPushButton(strings._("documents_add"))
self.add_btn.clicked.connect(self._on_add_clicked)
btn_row.addWidget(self.add_btn)
self.open_btn = QPushButton(strings._("documents_open"))
self.open_btn.clicked.connect(self._on_open_clicked)
btn_row.addWidget(self.open_btn)
self.delete_btn = QPushButton(strings._("documents_delete"))
self.delete_btn.clicked.connect(self._on_delete_clicked)
btn_row.addWidget(self.delete_btn)
close_btn = QPushButton(strings._("close"))
close_btn.clicked.connect(self.accept)
btn_row.addWidget(close_btn)
root.addLayout(btn_row)
# Separator at bottom (purely cosmetic)
line = QFrame()
line.setFrameShape(QFrame.HLine)
line.setFrameShadow(QFrame.Sunken)
root.addWidget(line)
# Init data
self._reload_projects()
self._select_initial_project(initial_project_id)
self._reload_documents()
# --- Helpers -------------------------------------------------------------
def _reload_projects(self) -> None:
self.project_combo.blockSignals(True)
try:
self.project_combo.clear()
for proj_id, name in self._db.list_projects():
self.project_combo.addItem(name, proj_id)
finally:
self.project_combo.blockSignals(False)
def _select_initial_project(self, project_id: Optional[int]) -> None:
if project_id is None:
if self.project_combo.count() > 0:
self.project_combo.setCurrentIndex(0)
return
idx = self.project_combo.findData(project_id)
if idx >= 0:
self.project_combo.setCurrentIndex(idx)
elif self.project_combo.count() > 0:
self.project_combo.setCurrentIndex(0)
def _current_project(self) -> Optional[int]:
idx = self.project_combo.currentIndex()
if idx < 0:
return None
proj_id = self.project_combo.itemData(idx)
return int(proj_id) if proj_id is not None else None
def _manage_projects(self) -> None:
dlg = TimeCodeManagerDialog(self._db, focus_tab="projects", parent=self)
dlg.exec()
self._reload_projects()
self._reload_documents()
def _on_search_text_changed(self, text: str) -> None:
"""Update the in-memory search text and reload the table."""
self._search_text = text
self._reload_documents()
def _reload_documents(self) -> None:
search = (self._search_text or "").strip()
self._reloading_docs = True
try:
self.table.setRowCount(0)
if search:
# Global search across all projects
rows: list[DocumentRow] = self._db.search_documents(search)
else:
proj_id = self._current_project()
if proj_id is None:
return
rows = self._db.documents_for_project(proj_id)
self.table.setRowCount(len(rows))
for row_idx, r in enumerate(rows):
(
doc_id,
_project_id,
project_name,
file_name,
description,
size_bytes,
uploaded_at,
) = r
# Col 0: File
file_item = QTableWidgetItem(file_name)
file_item.setData(Qt.ItemDataRole.UserRole, doc_id)
file_item.setFlags(file_item.flags() & ~Qt.ItemIsEditable)
self.table.setItem(row_idx, self.FILE_COL, file_item)
# Col 1: Tags (comma-separated)
tags = self._db.get_tags_for_document(doc_id)
tag_names = [name for (_tid, name, _color) in tags]
tags_text = ", ".join(tag_names)
tags_item = QTableWidgetItem(tags_text)
# If there is at least one tag, colour the cell using the first tag's colour
if tags:
first_color = tags[0][2]
if first_color:
col = QColor(first_color)
tags_item.setBackground(col)
# Choose a readable text color
if col.lightness() < 128:
tags_item.setForeground(QColor("#ffffff"))
else:
tags_item.setForeground(QColor("#000000"))
self.table.setItem(row_idx, self.TAGS_COL, tags_item)
if not self.cfg.tags:
self.table.hideColumn(self.TAGS_COL)
# Col 2: Description (editable)
desc_item = QTableWidgetItem(description or "")
self.table.setItem(row_idx, self.DESC_COL, desc_item)
# Col 3: Added at (not editable)
added_label = uploaded_at
added_item = QTableWidgetItem(added_label)
added_item.setFlags(added_item.flags() & ~Qt.ItemIsEditable)
self.table.setItem(row_idx, self.ADDED_COL, added_item)
# Col 4: Size (not editable)
size_item = QTableWidgetItem(self._format_size(size_bytes))
size_item.setFlags(size_item.flags() & ~Qt.ItemIsEditable)
self.table.setItem(row_idx, self.SIZE_COL, size_item)
finally:
self._reloading_docs = False
# --- Signals -------------------------------------------------------------
def _on_project_changed(self, idx: int) -> None:
_ = idx
self._reload_documents()
def _on_add_clicked(self) -> None:
proj_id = self._current_project()
if proj_id is None:
QMessageBox.warning(
self,
strings._("project_documents_title"),
strings._("documents_no_project_selected"),
)
return
paths, _ = QFileDialog.getOpenFileNames(
self,
strings._("documents_add"),
"",
strings._("documents_file_filter_all"),
)
if not paths:
return
for path in paths:
try:
self._db.add_document_from_path(proj_id, path)
except Exception as e: # pragma: no cover
QMessageBox.warning(
self,
strings._("project_documents_title"),
strings._("documents_add_failed").format(error=str(e)),
)
self._reload_documents()
def _selected_doc_meta(self) -> tuple[Optional[int], Optional[str]]:
row = self.table.currentRow()
if row < 0:
return None, None
file_item = self.table.item(row, self.FILE_COL)
if file_item is None:
return None, None
doc_id = file_item.data(Qt.ItemDataRole.UserRole)
file_name = file_item.text()
return (int(doc_id) if doc_id is not None else None, file_name)
def _on_open_clicked(self, *args) -> None:
doc_id, file_name = self._selected_doc_meta()
if doc_id is None or not file_name:
return
self._open_document(doc_id, file_name)
def _on_delete_clicked(self) -> None:
doc_id, _file_name = self._selected_doc_meta()
if doc_id is None:
return
resp = QMessageBox.question(
self,
strings._("project_documents_title"),
strings._("documents_confirm_delete"),
)
if resp != QMessageBox.StandardButton.Yes:
return
self._db.delete_document(doc_id)
self._reload_documents()
def _on_item_changed(self, item: QTableWidgetItem) -> None:
"""
Handle inline edits to Description and Tags.
"""
if self._reloading_docs or item is None:
return
row = item.row()
col = item.column()
file_item = self.table.item(row, self.FILE_COL)
if file_item is None:
return
doc_id = file_item.data(Qt.ItemDataRole.UserRole)
if doc_id is None:
return
doc_id = int(doc_id)
# Description column
if col == self.DESC_COL:
desc = item.text().strip() or None
self._db.update_document_description(doc_id, desc)
return
# Tags column
if col == self.TAGS_COL:
raw = item.text()
# split on commas, strip, drop empties
names = [p.strip() for p in raw.split(",") if p.strip()]
self._db.set_tags_for_document(doc_id, names)
# Re-normalise text to the canonical tag names stored in DB
tags = self._db.get_tags_for_document(doc_id)
tag_names = [name for (_tid, name, _color) in tags]
tags_text = ", ".join(tag_names)
self._reloading_docs = True
try:
item.setText(tags_text)
# Reset / apply background based on first tag colour
if tags:
first_color = tags[0][2]
if first_color:
col = QColor(first_color)
item.setBackground(col)
if col.lightness() < 128:
item.setForeground(QColor("#ffffff"))
else:
item.setForeground(QColor("#000000"))
else:
# No tags: clear background / foreground to defaults
item.setBackground(QColor())
item.setForeground(QColor())
finally:
self._reloading_docs = False
# --- utils -------------------------------------------------------------
def _open_document(self, doc_id: int, file_name: str) -> None:
"""
Fetch BLOB from DB, write to a temporary file, and open with default app.
"""
try:
data = self._db.document_data(doc_id)
except Exception as e:
QMessageBox.warning(
self,
strings._("project_documents_title"),
strings._("documents_open_failed").format(error=str(e)),
)
return
suffix = Path(file_name).suffix or ""
tmp = tempfile.NamedTemporaryFile(
prefix="bouquin_doc_",
suffix=suffix,
delete=False,
)
try:
tmp.write(data)
tmp.flush()
finally:
tmp.close()
QDesktopServices.openUrl(QUrl.fromLocalFile(tmp.name))
@staticmethod
def _format_size(size_bytes: int) -> str:
"""
Human-readable file size.
"""
if size_bytes < 1024:
return f"{size_bytes} B"
kb = size_bytes / 1024.0
if kb < 1024:
return f"{kb:.1f} KB"
mb = kb / 1024.0
if mb < 1024:
return f"{mb:.1f} MB"
gb = mb / 1024.0
return f"{gb:.1f} GB"

View file

@ -142,6 +142,7 @@
"tag_browser_instructions": "Click a tag to expand and see all pages with that tag. Click a date to open it. Select a tag to edit its name, change its color, or delete it globally.",
"color_hex": "Colour",
"date": "Date",
"page_or_document": "Page / Document",
"add_a_tag": "Add a tag",
"edit_tag_name": "Edit tag name",
"new_tag_name": "New tag name:",
@ -161,6 +162,9 @@
"stats_heatmap_metric": "Colour by",
"stats_metric_words": "Words",
"stats_metric_revisions": "Revisions",
"stats_metric_documents": "Documents",
"stats_total_documents": "Total documents",
"stats_date_most_documents": "Date with most documents",
"stats_no_data": "No statistics available yet.",
"select_notebook": "Select notebook",
"bug_report_explanation": "Describe what went wrong, what you expected to happen, and any steps to reproduce.\n\nWe do not collect anything else except the Bouquin version number.\n\nIf you wish to be contacted, please leave contact information.\n\nYour request will be sent over HTTPS.",
@ -261,6 +265,7 @@
"enable_tags_feature": "Enable Tags",
"enable_time_log_feature": "Enable Time Logging",
"enable_reminders_feature": "Enable Reminders",
"enable_documents_feature": "Enable storing of documents",
"pomodoro_time_log_default_text": "Focus session",
"toolbar_pomodoro_timer": "Time-logging timer",
"set_code_language": "Set code language",
@ -293,5 +298,28 @@
"sunday": "Sunday",
"day": "Day",
"edit_code_block": "Edit code block",
"delete_code_block": "Delete code block"
"delete_code_block": "Delete code block",
"search_result_heading_document": "Document",
"toolbar_documents": "Documents Manager",
"project_documents_title": "Project documents",
"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)",
"todays_documents": "Documents from this day",
"todays_documents_none": "No documents yet."
}

View file

@ -50,6 +50,7 @@ from PySide6.QtWidgets import (
from .bug_report_dialog import BugReportDialog
from .db import DBManager
from .documents import DocumentsDialog, TodaysDocumentsWidget
from .find_bar import FindBar
from .history_dialog import HistoryDialog
from .key_prompt import KeyPrompt
@ -126,6 +127,8 @@ class MainWindow(QMainWindow):
left_layout.addWidget(self.calendar)
left_layout.addWidget(self.search)
left_layout.addWidget(self.upcoming_reminders)
self.todays_documents = TodaysDocumentsWidget(self.db, self._current_date_iso())
left_layout.addWidget(self.todays_documents)
left_layout.addWidget(self.time_log)
left_layout.addWidget(self.tags)
left_panel.setFixedWidth(self.calendar.sizeHint().width() + 16)
@ -335,6 +338,9 @@ class MainWindow(QMainWindow):
if not self.cfg.reminders:
self.upcoming_reminders.hide()
self.toolBar.actAlarm.setVisible(False)
if not self.cfg.documents:
self.todays_documents.hide()
self.toolBar.actDocuments.setVisible(False)
# Restore window position from settings
self._restore_window_position()
@ -1091,6 +1097,7 @@ class MainWindow(QMainWindow):
self._tb_checkboxes = lambda: self._call_editor("toggle_checkboxes")
self._tb_alarm = self._on_alarm_requested
self._tb_timer = self._on_timer_requested
self._tb_documents = self._on_documents_requested
self._tb_font_larger = self._on_font_larger_requested
self._tb_font_smaller = self._on_font_smaller_requested
@ -1104,6 +1111,7 @@ class MainWindow(QMainWindow):
tb.checkboxesRequested.connect(self._tb_checkboxes)
tb.alarmRequested.connect(self._tb_alarm)
tb.timerRequested.connect(self._tb_timer)
tb.documentsRequested.connect(self._tb_documents)
tb.insertImageRequested.connect(self._on_insert_image)
tb.historyRequested.connect(self._open_history)
tb.fontSizeLargerRequested.connect(self._tb_font_larger)
@ -1320,6 +1328,14 @@ class MainWindow(QMainWindow):
timer.start(msecs)
self._reminder_timers.append(timer)
# ----------- Documents handler ------------#
def _on_documents_requested(self):
documents_dlg = DocumentsDialog(self.db, self)
documents_dlg.exec()
# Refresh recent documents after any changes
if hasattr(self, "todays_documents"):
self.todays_documents.reload()
# ----------- History handler ------------#
def _open_history(self):
if hasattr(self.editor, "current_date"):
@ -1354,6 +1370,8 @@ class MainWindow(QMainWindow):
self.tags.set_current_date(date_iso)
if hasattr(self, "time_log"):
self.time_log.set_current_date(date_iso)
if hasattr(self, "todays_documents"):
self.todays_documents.set_current_date(date_iso)
def _on_tag_added(self):
"""Called when a tag is added - trigger autosave for current page"""
@ -1421,6 +1439,7 @@ class MainWindow(QMainWindow):
self.cfg.tags = getattr(new_cfg, "tags", self.cfg.tags)
self.cfg.time_log = getattr(new_cfg, "time_log", self.cfg.time_log)
self.cfg.reminders = getattr(new_cfg, "reminders", self.cfg.reminders)
self.cfg.documents = getattr(new_cfg, "documents", self.cfg.documents)
self.cfg.locale = getattr(new_cfg, "locale", self.cfg.locale)
self.cfg.font_size = getattr(new_cfg, "font_size", self.cfg.font_size)
@ -1458,6 +1477,12 @@ class MainWindow(QMainWindow):
else:
self.upcoming_reminders.show()
self.toolBar.actAlarm.setVisible(True)
if not self.cfg.documents:
self.todays_documents.hide()
self.toolBar.actDocuments.setVisible(False)
else:
self.todays_documents.show()
self.toolBar.actDocuments.setVisible(True)
# ------------ Statistics handler --------------- #

View file

@ -129,8 +129,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, rounded up
hours = math.ceil(elapsed_seconds / 360) / 25 # Round up to nearest 0.25 hour
# 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:

View file

@ -18,7 +18,7 @@ from PySide6.QtWidgets import (
from . import strings
Row = Tuple[str, str]
Row = Tuple[str, str, str, str, str | None]
class Search(QWidget):
@ -52,9 +52,55 @@ class Search(QWidget):
lay.addWidget(self.results)
def _open_selected(self, item: QListWidgetItem):
date_str = item.data(Qt.ItemDataRole.UserRole)
if date_str:
self.openDateRequested.emit(date_str)
data = item.data(Qt.ItemDataRole.UserRole)
if not isinstance(data, dict):
return
kind = data.get("kind")
if kind == "page":
date_iso = data.get("date")
if date_iso:
self.openDateRequested.emit(date_iso)
elif kind == "document":
doc_id = data.get("doc_id")
file_name = data.get("file_name") or "document"
if doc_id is None:
return
self._open_document(int(doc_id), file_name)
def _open_document(self, doc_id: int, file_name: str) -> None:
"""
Open a document search result via a temp file.
"""
from pathlib import Path
import tempfile
from PySide6.QtCore import QUrl
from PySide6.QtGui import QDesktopServices
from PySide6.QtWidgets import QMessageBox
try:
data = self._db.document_data(doc_id)
except Exception as e:
QMessageBox.warning(
self,
strings._("project_documents_title"),
strings._("documents_open_failed").format(error=str(e)),
)
return
suffix = Path(file_name).suffix or ""
tmp = tempfile.NamedTemporaryFile(
prefix="bouquin_doc_",
suffix=suffix,
delete=False,
)
try:
tmp.write(data)
tmp.flush()
finally:
tmp.close()
QDesktopServices.openUrl(QUrl.fromLocalFile(tmp.name))
def _search(self, text: str):
"""
@ -80,28 +126,28 @@ class Search(QWidget):
self.resultDatesChanged.emit([]) # clear highlights
return
self.resultDatesChanged.emit(sorted({d for d, _ in rows}))
# Only highlight calendar dates for page results
page_dates = sorted(
{key for (kind, key, _title, _text, _aux) in rows if kind == "page"}
)
self.resultDatesChanged.emit(page_dates)
self.results.show()
for date_str, content in rows:
# Build an HTML fragment around the match and whether to show ellipses
frag_html = self._make_html_snippet(content, query, radius=30, maxlen=90)
# ---- Per-item widget: date on top, preview row below (with ellipses) ----
for kind, key, title, text, aux in rows:
# Build an HTML fragment around the match
frag_html = self._make_html_snippet(text, query, radius=30, maxlen=90)
container = QWidget()
outer = QVBoxLayout(container)
outer.setContentsMargins(8, 6, 8, 6)
outer.setContentsMargins(0, 0, 0, 0)
outer.setSpacing(2)
# Date label (plain text)
date_lbl = QLabel()
date_lbl.setTextFormat(Qt.TextFormat.RichText)
date_lbl.setText(f"<h3><i>{date_str}</i></h3>")
date_f = date_lbl.font()
date_f.setPointSizeF(date_f.pointSizeF() + 1)
date_lbl.setFont(date_f)
outer.addWidget(date_lbl)
# ---- Heading (date for pages, "Document" for docs) ----
heading = QLabel(title)
heading.setStyleSheet("font-weight:bold;")
outer.addWidget(heading)
# Preview row with optional ellipses
# ---- Preview row ----
row = QWidget()
h = QHBoxLayout(row)
h.setContentsMargins(0, 0, 0, 0)
@ -117,9 +163,9 @@ class Search(QWidget):
else "<span style='color:#888'>(no preview)</span>"
)
h.addWidget(preview, 1)
outer.addWidget(row)
# Separator line
line = QFrame()
line.setFrameShape(QFrame.HLine)
line.setFrameShadow(QFrame.Sunken)
@ -127,9 +173,22 @@ class Search(QWidget):
# ---- Add to list ----
item = QListWidgetItem()
item.setData(Qt.ItemDataRole.UserRole, date_str)
item.setSizeHint(container.sizeHint())
if kind == "page":
item.setData(
Qt.ItemDataRole.UserRole,
{"kind": "page", "date": key},
)
else: # document
item.setData(
Qt.ItemDataRole.UserRole,
{
"kind": "document",
"doc_id": int(key),
"file_name": aux or "",
},
)
item.setSizeHint(container.sizeHint())
self.results.addItem(item)
self.results.setItemWidget(item, container)

View file

@ -44,6 +44,7 @@ def load_db_config() -> DBConfig:
tags = s.value("ui/tags", True, type=bool)
time_log = s.value("ui/time_log", True, type=bool)
reminders = s.value("ui/reminders", True, type=bool)
documents = s.value("ui/documents", True, type=bool)
locale = s.value("ui/locale", "en", type=str)
font_size = s.value("ui/font_size", 11, type=int)
return DBConfig(
@ -55,6 +56,7 @@ def load_db_config() -> DBConfig:
tags=tags,
time_log=time_log,
reminders=reminders,
documents=documents,
locale=locale,
font_size=font_size,
)
@ -70,5 +72,6 @@ def save_db_config(cfg: DBConfig) -> None:
s.setValue("ui/tags", str(cfg.tags))
s.setValue("ui/time_log", str(cfg.time_log))
s.setValue("ui/reminders", str(cfg.reminders))
s.setValue("ui/documents", str(cfg.documents))
s.setValue("ui/locale", str(cfg.locale))
s.setValue("ui/font_size", str(cfg.font_size))

View file

@ -181,6 +181,11 @@ class SettingsDialog(QDialog):
self.reminders.setCursor(Qt.PointingHandCursor)
features_layout.addWidget(self.reminders)
self.documents = QCheckBox(strings._("enable_documents_feature"))
self.documents.setChecked(self.current_settings.documents)
self.documents.setCursor(Qt.PointingHandCursor)
features_layout.addWidget(self.documents)
layout.addWidget(features_group)
layout.addStretch()
return page
@ -308,6 +313,7 @@ class SettingsDialog(QDialog):
tags=self.tags.isChecked(),
time_log=self.time_log.isChecked(),
reminders=self.reminders.isChecked(),
documents=self.documents.isChecked(),
locale=self.locale_combobox.currentText(),
font_size=self.font_size.value(),
)

View file

@ -20,6 +20,7 @@ from PySide6.QtWidgets import (
from . import strings
from .db import DBManager
from .settings import load_db_config
# ---------- Activity heatmap ----------
@ -265,6 +266,32 @@ class StatisticsDialog(QDialog):
revisions_by_date,
) = self._gather_stats()
# Optional: per-date document counts for the heatmap.
# This uses project_documents.uploaded_at aggregated by day, if the
# Documents feature is enabled.
self.cfg = load_db_config()
documents_by_date: Dict[_dt.date, int] = {}
total_documents = 0
date_most_documents: _dt.date | None = None
date_most_documents_count = 0
if self.cfg.documents:
try:
documents_by_date = self._db.documents_by_date() or {}
except Exception:
documents_by_date = {}
if documents_by_date:
total_documents = sum(documents_by_date.values())
# Choose the date with the highest count, tie-breaking by earliest date.
date_most_documents, date_most_documents_count = sorted(
documents_by_date.items(),
key=lambda item: (-item[1], item[0]),
)[0]
# for the heatmap
self._documents_by_date = documents_by_date
# --- Numeric summary at the top ----------------------------------
form = QFormLayout()
root.addLayout(form)
@ -291,7 +318,8 @@ class StatisticsDialog(QDialog):
QLabel(str(total_words)),
)
# Unique tag names
# Tags
if self.cfg.tags:
form.addRow(
strings._("stats_unique_tags"),
QLabel(str(unique_tags)),
@ -305,8 +333,24 @@ class StatisticsDialog(QDialog):
else:
form.addRow(strings._("stats_page_most_tags"), QLabel(""))
# Documents
if date_most_documents:
form.addRow(
strings._("stats_total_documents"),
QLabel(str(total_documents)),
)
doc_most_label = (
f"{date_most_documents.isoformat()} ({date_most_documents_count})"
)
form.addRow(
strings._("stats_date_most_documents"),
QLabel(doc_most_label),
)
# --- Heatmap with switcher ---------------------------------------
if words_by_date or revisions_by_date:
if words_by_date or revisions_by_date or documents_by_date:
group = QGroupBox(strings._("stats_activity_heatmap"))
group_layout = QVBoxLayout(group)
@ -316,6 +360,10 @@ class StatisticsDialog(QDialog):
self.metric_combo = QComboBox()
self.metric_combo.addItem(strings._("stats_metric_words"), "words")
self.metric_combo.addItem(strings._("stats_metric_revisions"), "revisions")
if documents_by_date:
self.metric_combo.addItem(
strings._("stats_metric_documents"), "documents"
)
combo_row.addWidget(self.metric_combo)
combo_row.addStretch(1)
group_layout.addLayout(combo_row)
@ -344,6 +392,8 @@ class StatisticsDialog(QDialog):
def _apply_metric(self, metric: str) -> None:
if metric == "revisions":
self._heatmap.set_data(self._revisions_by_date)
elif metric == "documents":
self._heatmap.set_data(self._documents_by_date)
else:
self._heatmap.set_data(self._words_by_date)

View file

@ -1,5 +1,5 @@
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QColor
from PySide6.QtCore import Qt, Signal, QUrl
from PySide6.QtGui import QColor, QDesktopServices
from PySide6.QtWidgets import (
QDialog,
QVBoxLayout,
@ -13,7 +13,11 @@ from PySide6.QtWidgets import (
QInputDialog,
)
from pathlib import Path
import tempfile
from .db import DBManager
from .settings import load_db_config
from . import strings
from sqlcipher3.dbapi2 import IntegrityError
@ -25,6 +29,7 @@ class TagBrowserDialog(QDialog):
def __init__(self, db: DBManager, parent=None, focus_tag: str | None = None):
super().__init__(parent)
self._db = db
self.cfg = load_db_config()
self.setWindowTitle(
strings._("tag_browser_title") + " / " + strings._("manage_tags")
)
@ -38,9 +43,18 @@ class TagBrowserDialog(QDialog):
layout.addWidget(instructions)
self.tree = QTreeWidget()
if not self.cfg.documents:
self.tree.setHeaderLabels(
[strings._("tag"), strings._("color_hex"), strings._("date")]
)
else:
self.tree.setHeaderLabels(
[
strings._("tag"),
strings._("color_hex"),
strings._("page_or_document"),
]
)
self.tree.setColumnWidth(0, 200)
self.tree.setColumnWidth(1, 100)
self.tree.itemActivated.connect(self._on_item_activated)
@ -119,6 +133,7 @@ class TagBrowserDialog(QDialog):
self.tree.addTopLevelItem(root)
# Pages with this tag
pages = self._db.get_pages_for_tag(name)
for date_iso, _content in pages:
child = QTreeWidgetItem(["", "", date_iso])
@ -127,6 +142,21 @@ class TagBrowserDialog(QDialog):
)
root.addChild(child)
# Documents with this tag
if self.cfg.documents:
docs = self._db.get_documents_for_tag(name)
for doc_id, project_name, file_name in docs:
label = file_name
if project_name:
label = f"{file_name} ({project_name})"
child = QTreeWidgetItem(["", "", label])
child.setData(
0,
Qt.ItemDataRole.UserRole,
{"type": "document", "id": doc_id},
)
root.addChild(child)
if focus_tag and name.lower() == focus_tag.lower():
focus_item = root
@ -153,12 +183,45 @@ class TagBrowserDialog(QDialog):
def _on_item_activated(self, item: QTreeWidgetItem, column: int):
data = item.data(0, Qt.ItemDataRole.UserRole)
if isinstance(data, dict):
if data.get("type") == "page":
item_type = data.get("type")
if item_type == "page":
date_iso = data.get("date")
if date_iso:
self.openDateRequested.emit(date_iso)
self.accept()
elif item_type == "document":
doc_id = data.get("id")
if doc_id is not None:
self._open_document(int(doc_id), str(data.get("file_name")))
def _open_document(self, doc_id: int, file_name: str) -> None:
"""Open a tagged document via the default external application."""
try:
data = self._db.document_data(doc_id)
except Exception as e:
QMessageBox.warning(
self,
strings._("project_documents_title"),
strings._("documents_open_failed").format(error=str(e)),
)
return
suffix = Path(file_name).suffix or ""
tmp = tempfile.NamedTemporaryFile(
prefix="bouquin_doc_",
suffix=suffix,
delete=False,
)
try:
tmp.write(data)
tmp.flush()
finally:
tmp.close()
QDesktopServices.openUrl(QUrl.fromLocalFile(tmp.name))
def _add_a_tag(self):
"""Add a new tag"""

View file

@ -185,7 +185,12 @@ class TimeLogWidget(QFrame):
return
dlg = TimeLogDialog(
self._db, self._current_date, self, True, themes=self._themes
self._db,
self._current_date,
self,
True,
themes=self._themes,
close_after_add=True,
)
dlg.exec()
@ -214,6 +219,7 @@ class TimeLogDialog(QDialog):
parent=None,
log_entry_only: bool | None = False,
themes: ThemeManager | None = None,
close_after_add: bool | None = False,
):
super().__init__(parent)
self._db = db
@ -224,6 +230,8 @@ class TimeLogDialog(QDialog):
# programmatic item changes as user edits.
self._reloading_entries: bool = False
self.close_after_add = close_after_add
self.setWindowTitle(strings._("time_log_for").format(date=date_iso))
self.resize(900, 600)
@ -488,6 +496,8 @@ class TimeLogDialog(QDialog):
)
self._reload_entries()
if self.close_after_add:
self.close()
def _on_row_selected(self) -> None:
items = self.table.selectedItems()

View file

@ -20,6 +20,7 @@ class ToolBar(QToolBar):
insertImageRequested = Signal()
alarmRequested = Signal()
timerRequested = Signal()
documentsRequested = Signal()
fontSizeLargerRequested = Signal()
fontSizeSmallerRequested = Signal()
@ -120,6 +121,11 @@ class ToolBar(QToolBar):
self.actTimer.setToolTip(strings._("toolbar_pomodoro_timer"))
self.actTimer.triggered.connect(self.timerRequested)
# Documents
self.actDocuments = QAction("📁", self)
self.actDocuments.setToolTip(strings._("toolbar_documents"))
self.actDocuments.triggered.connect(self.documentsRequested)
# Set exclusive buttons in QActionGroups
self.grpHeadings = QActionGroup(self)
self.grpHeadings.setExclusive(True)
@ -159,6 +165,7 @@ class ToolBar(QToolBar):
self.actInsertImg,
self.actAlarm,
self.actTimer,
self.actDocuments,
self.actHistory,
]
)
@ -185,6 +192,7 @@ class ToolBar(QToolBar):
self._style_letter_button(self.actCheckboxes, "")
self._style_letter_button(self.actAlarm, "")
self._style_letter_button(self.actTimer, "")
self._style_letter_button(self.actDocuments, "📁")
# History
self._style_letter_button(self.actHistory, "")

326
poetry.lock generated
View file

@ -267,13 +267,13 @@ xdg-desktop-portal = ["jeepney"]
[[package]]
name = "exceptiongroup"
version = "1.3.0"
version = "1.3.1"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
files = [
{file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"},
{file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"},
{file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"},
{file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"},
]
[package.dependencies]
@ -380,57 +380,57 @@ tomli = {version = "*", markers = "python_version < \"3.11\""}
[[package]]
name = "pyside6"
version = "6.10.0"
version = "6.10.1"
description = "Python bindings for the Qt cross-platform application and UI framework"
optional = false
python-versions = "<3.14,>=3.9"
python-versions = "<3.15,>=3.9"
files = [
{file = "pyside6-6.10.0-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:c2cbc5dc2a164e3c7c51b3435e24203e90e5edd518c865466afccbd2e5872bb0"},
{file = "pyside6-6.10.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ae8c3c8339cd7c3c9faa7cc5c52670dcc8662ccf4b63a6fed61c6345b90c4c01"},
{file = "pyside6-6.10.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:9f402f883e640048fab246d36e298a5e16df9b18ba2e8c519877e472d3602820"},
{file = "pyside6-6.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:70a8bcc73ea8d6baab70bba311eac77b9a1d31f658d0b418e15eb6ea36c97e6f"},
{file = "pyside6-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:4b709bdeeb89d386059343a5a706fc185cee37b517bda44c7d6b64d5fdaf3339"},
{file = "pyside6-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:d0e70dd0e126d01986f357c2a555722f9462cf8a942bf2ce180baf69f468e516"},
{file = "pyside6-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4053bf51ba2c2cb20e1005edd469997976a02cec009f7c46356a0b65c137f1fa"},
{file = "pyside6-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:7d3ca20a40139ca5324a7864f1d91cdf2ff237e11bd16354a42670f2a4eeb13c"},
{file = "pyside6-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:9f89ff994f774420eaa38cec6422fddd5356611d8481774820befd6f3bb84c9e"},
{file = "pyside6-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:9c5c1d94387d1a32a6fae25348097918ef413b87dfa3767c46f737c6d48ae437"},
]
[package.dependencies]
PySide6_Addons = "6.10.0"
PySide6_Essentials = "6.10.0"
shiboken6 = "6.10.0"
PySide6_Addons = "6.10.1"
PySide6_Essentials = "6.10.1"
shiboken6 = "6.10.1"
[[package]]
name = "pyside6-addons"
version = "6.10.0"
version = "6.10.1"
description = "Python bindings for the Qt cross-platform application and UI framework (Addons)"
optional = false
python-versions = "<3.14,>=3.9"
python-versions = "<3.15,>=3.9"
files = [
{file = "pyside6_addons-6.10.0-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:88e61e21ee4643cdd9efb39ec52f4dc1ac74c0b45c5b7fa453d03c094f0a8a5c"},
{file = "pyside6_addons-6.10.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:08d4ed46c4c9a353a9eb84134678f8fdd4ce17fb8cce2b3686172a7575025464"},
{file = "pyside6_addons-6.10.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:15d32229d681be0bba1b936c4a300da43d01e1917ada5b57f9e03a387c245ab0"},
{file = "pyside6_addons-6.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:99d93a32c17c5f6d797c3b90dd58f2a8bae13abde81e85802c34ceafaee11859"},
{file = "pyside6_addons-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:92536427413f3b6557cf53f1a515cd766725ee46a170aff57ad2ff1dfce0ffb1"},
{file = "pyside6_addons-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:4d2b82bbf9b861134845803837011e5f9ac7d33661b216805273cf0c6d0f8e82"},
{file = "pyside6_addons-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:330c229b58d30083a7b99ed22e118eb4f4126408429816a4044ccd0438ae81b4"},
{file = "pyside6_addons-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:56864b5fecd6924187a2d0f7e98d968ed72b6cc267caa5b294cd7e88fff4e54c"},
{file = "pyside6_addons-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:b6e249d15407dd33d6a2ffabd9dc6d7a8ab8c95d05f16a71dad4d07781c76341"},
{file = "pyside6_addons-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:0de303c0447326cdc6c8be5ab066ef581e2d0baf22560c9362d41b8304fdf2db"},
]
[package.dependencies]
PySide6_Essentials = "6.10.0"
shiboken6 = "6.10.0"
PySide6_Essentials = "6.10.1"
shiboken6 = "6.10.1"
[[package]]
name = "pyside6-essentials"
version = "6.10.0"
version = "6.10.1"
description = "Python bindings for the Qt cross-platform application and UI framework (Essentials)"
optional = false
python-versions = "<3.14,>=3.9"
python-versions = "<3.15,>=3.9"
files = [
{file = "pyside6_essentials-6.10.0-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:003e871effe1f3e5b876bde715c15a780d876682005a6e989d89f48b8b93e93a"},
{file = "pyside6_essentials-6.10.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1d5e013a8698e37ab8ef360e6960794eb5ef20832a8d562e649b8c5a0574b2d8"},
{file = "pyside6_essentials-6.10.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:b1dd0864f0577a448fb44426b91cafff7ee7cccd1782ba66491e1c668033f998"},
{file = "pyside6_essentials-6.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:fc167eb211dd1580e20ba90d299e74898e7a5a1306d832421e879641fc03b6fe"},
{file = "pyside6_essentials-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:6dd0936394cb14da2fd8e869899f5e0925a738b1c8d74c2f22503720ea363fb1"},
{file = "pyside6_essentials-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:cd224aff3bb26ff1fca32c050e1c4d0bd9f951a96219d40d5f3d0128485b0bbe"},
{file = "pyside6_essentials-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:e9ccbfb58c03911a0bce1f2198605b02d4b5ca6276bfc0cbcf7c6f6393ffb856"},
{file = "pyside6_essentials-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:ec8617c9b143b0c19ba1cc5a7e98c538e4143795480cb152aee47802c18dc5d2"},
{file = "pyside6_essentials-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:9555a48e8f0acf63fc6a23c250808db841b28a66ed6ad89ee0e4df7628752674"},
{file = "pyside6_essentials-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:4d1d248644f1778f8ddae5da714ca0f5a150a5e6f602af2765a7d21b876da05c"},
]
[package.dependencies]
shiboken6 = "6.10.0"
shiboken6 = "6.10.1"
[[package]]
name = "pytest"
@ -534,147 +534,153 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "shiboken6"
version = "6.10.0"
version = "6.10.1"
description = "Python/C++ bindings helper module"
optional = false
python-versions = "<3.14,>=3.9"
python-versions = "<3.15,>=3.9"
files = [
{file = "shiboken6-6.10.0-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:7a5f5f400ebfb3a13616030815708289c2154e701a60b9db7833b843e0bee543"},
{file = "shiboken6-6.10.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:e612734da515d683696980107cdc0396a3ae0f07b059f0f422ec8a2333810234"},
{file = "shiboken6-6.10.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:b01377e68d14132360efb0f4b7233006d26aa8ae9bb50edf00960c2a5f52d148"},
{file = "shiboken6-6.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:0bc5631c1bf150cbef768a17f5f289aae1cb4db6c6b0c19b2421394e27783717"},
{file = "shiboken6-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:dfc4beab5fec7dbbebbb418f3bf99af865d6953aa0795435563d4cbb82093b61"},
{file = "shiboken6-6.10.1-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:9f2990f5b61b0b68ecadcd896ab4441f2cb097eef7797ecc40584107d9850d71"},
{file = "shiboken6-6.10.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4221a52dfb81f24a0d20cc4f8981cb6edd810d5a9fb28287ce10d342573a0e4"},
{file = "shiboken6-6.10.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:c095b00f4d6bf578c0b2464bb4e264b351a99345374478570f69e2e679a2a1d0"},
{file = "shiboken6-6.10.1-cp39-abi3-win_amd64.whl", hash = "sha256:c1601d3cda1fa32779b141663873741b54e797cb0328458d7466281f117b0a4e"},
{file = "shiboken6-6.10.1-cp39-abi3-win_arm64.whl", hash = "sha256:5cf800917008587b551005a45add2d485cca66f5f7ecd5b320e9954e40448cc9"},
]
[[package]]
name = "sqlcipher3-wheels"
version = "0.5.5.post0"
version = "0.5.6"
description = "DB-API 2.0 interface for SQLCipher 3.x"
optional = false
python-versions = "*"
files = [
{file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:836cff85673ab9bdfe0f3e2bc38aefddb5f3a4c0de397b92f83546bb94ea38aa"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3fde9076a8810d19044f65fdfeeee5a9d044176ce91adc2258c8b18cb945474"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ad3ccb27f3fc9260b1bcebfd33fc5af1c2a1bf6a50e8e1bf7991d492458b438"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94bb8ab8cf7ae3dc0d51dcb75bf242ae4bd2f18549bfc975fd696c181e9ea8ca"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df0bf0a169b480615ea2021e7266e1154990762216d1fd8105b93d1fee336f49"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79cc1af145345e9bd625c961e4efc8fc6c6eefcaec90fbcf1c6b981492c08031"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d8b9f1c6d283acc5a0da16574c0f7690ba5b14cb5935f3078ccf8404a530075"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:952a23069a149a192a5eb8a9e552772b38c012825238175bc810f445a3aa8000"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:24f1a57a4aa18d9ecd38cfce69dd06e58cfb521151a8316e18183e603e7108f4"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6568c64adc55f9882ba36c11a446810bd5d4c03796aab8ecb9024f3bca9eb2cd"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:26c2b58d2f2a9dd23ad4c310fb6c0f0c82ca4f36a0d4177a70f0efeb332798ee"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:46827ffc7e705c5ecdf23ec69f56dd55b20857dc3c3c4893e360de8a38b4e708"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4033bbe2f0342936736ce7b8b2626f532509315576d5376764b410deae181cad"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-win32.whl", hash = "sha256:bfb26dbba945860427bd3f82c132e6d2ef409baa062d315b952dd5a930b25870"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-win_amd64.whl", hash = "sha256:168270b8fb295314aa4ee9f74435ceee42207bd16fe908f646a829b3a9daedad"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp310-cp310-win_arm64.whl", hash = "sha256:1f1bb2c4c6defa812eb0238055a283cf3c2f400e956a57c25cf65cbdbac6783f"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:55d557376a90f14baf0f35e917f8644c3a8cf48897947fcd7ecf51d490dd689f"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f1739264a971901088fe1670befb8a8a707543186c8eecc58158ca287e309b2"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:920e19345b6c5335b61e9fbed2625f96dbc3b0269ab5120baeae2c9288f0be01"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6462734f6d0703f863f5968419d229de75bbf2a829f762bfb257b6df2355f977"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c397305b65612da76f254c692ff866571aa98fd3817ed0e40fce6d568d704966"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf3467fe834075b58215c50f9db7355ef86a73d256ac8ba5fffb8c946741a5dc"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a76d783c095a3c95185757c418e3bad3eab69cbf986970d422cce5431e84d7f5"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bf8d78895ee0f04dc525942a1f40796fa7c3d7d7fb36c987f55c243ce34192d"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d583a10dbe9a1752968788c2d6438461ec7068608ceaa72e6468d80727c3152e"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8c3156b39bb8f24dfbe17a49126d8fa404b00c01d7aa84e64a2293db1dae1a38"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:15c3cf77b31973aa008174518fa439d8620a093964c2d9edcb8d23d543345839"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f5743db0f3492359c2ab3a56b6bed00ecba193f2c75c74e8e3d78a45f1eb7c95"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cc40a213a3633f19c96432304a16f0cff7c4aeca1a3d2042d4be36e576e64a70"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-win32.whl", hash = "sha256:433456ce962ae50887d6428d55bad46e5748a2cdd3d036180eb0bcdbe8bae9f9"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-win_amd64.whl", hash = "sha256:ca4332b1890cc4f80587be8bd529e20475bd3291e07f11115b1fc773947b264a"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp311-cp311-win_arm64.whl", hash = "sha256:a4634300cb2440baf17a78d6481d10902de4a2a6518f83a5ab2fe081e6b20b42"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8df43c11d767c6aac5cc300c1957356a9fd1b25f1946891003cf20a0146241"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:797653f08ecffcef2948dfd907fb20dab402d9efde6217c96befafc236e96c5b"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dca428fde0d1a522473f766465324d6539d324f219f4f7c159a3eb0d4f9983c5"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48e97922240a04b44637eabf39f86d243fe61fe7db1bd2ad219eb4053158f263"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8d3a366e52a6732b1ccff14f9ca77ecbee53abfce87c417bf05d4301484584f"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dce28a2431260251d7acf253ea1950983e48dfec64245126b39a770d5a88f507"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea64cce27152cae453c353038336bda0dc1f885e5e8e30b5cd28b8c9b498bbeb"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02d9e6120a496f083c525efc34408d4f2ca282da05bebcc967a0aa1e12a0d6ca"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:875cfc61bbf694b8327c2485e5ed40573e8b715f4e583502f12c51c8d5a92dd5"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0e9ed3ff9c00ba3888f8dbc0c7c84377ef66f21c5f4ac373fc690dcf5e9bd594"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:08ad6d767502429e497b6d03b5ae11e43e896d36f05ac8e60c12d8f124378bc1"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ecdefdcf7ab8cb14b3147a59af83e8e3e5e3bed46fc43ab86a657f5c306a83d2"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:75ffe5677407bf20a32486eb6facfbb07a353ce7c9aecc9fefd4e9d3275605d7"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-win32.whl", hash = "sha256:97b6c6556b430b5c0dff53e8f709f90ba53294c2a3958a8c38f573c6dbf467d9"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-win_amd64.whl", hash = "sha256:248cae211991f1ffb3a885a1223e62abee70c6c208fc2224a8dbf73d4e825baa"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp312-cp312-win_arm64.whl", hash = "sha256:5a49fc3a859a53fd025dc2fa08410292d267018897fc63198de6c92860fa8be7"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2f798e1591fa5ba14d9da08a54f18e7000fd74973cde12eb862a3928a69b7996"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:369011b8dc68741313a8b77bb68a70b76052390eaf819e4cd6e13d0acbea602d"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5658990462a728c1f4b472d23c1f7f577eb2bced5bbbf7c2b45158b8340484bd"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:907a166e4e563da67fe22c480244459512e32d3e00853b3f1e6fdb9da6aa2da6"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ba972405e9f16042e37cbcb4fef47248339c8410847390d41268bd45dc3f6ca"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b5f1b380fe3b869f701f9d2a8c09e9edfeec261573c8bb009a3336717260d65"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2978c9d964ad643b0bc61e19d8d608a515ff270e9a2f1f6c5aeb8ad56255def"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29457feb1516a2542aa7676e6d03bf913191690bf1ed6c82353782a380388508"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:021af568414741a45bfca41d682da64916a7873498a31d896cc34ad540939c6b"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7471d12eef489eea60cc3806bae0690f6d0733f7aea371a3ad5c5642f3bc04a9"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0f5faf683db9ade192a870e28b1eeeec2eb0aeca18e88fa52195a4639974c7cb"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:64fe6bac67d2b807b91102eef41d7f389e008ded80575ba597b454e05f9522e5"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:63ef1e23eb9729e79783e2ab4b19f64276c155ba0a85ba1eeb21e248c6ce0956"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-win32.whl", hash = "sha256:4eafde00138dd3753085b4f5eab0811247207b699de862589f886a94ad3628a4"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-win_amd64.whl", hash = "sha256:909864f275460646d0bf5475dc42e9c2cadd79cd40805ea32fe9a69300595301"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp313-cp313-win_arm64.whl", hash = "sha256:a831846cc6b01d7f99576efbf797b61a269dffa6885f530b6957573ce1a24f10"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:aaad03f3eb099401306fead806908c85b923064a9da7a99c33a72c3b7c9143bf"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74d4b4cde184d9e354152fd1867bcbaee468529865703ad863840a0ce4eb60cd"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac3ad3cf9e1d0f08e8d22a65115368f2b22b9e96403fa644e146e1221c65c454"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45c4d3dca4acdbc5543bb00aee1e0715db797aa2819db5b7ca3feed3ab3366ff"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf7026851ea60d63c1a88f62439da78b68bfbfec192c781255e3cfb34b6efc12"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7ae4a83678c41c2cdbf3c2b18fc46be32225260c7b4807087bdb43793ee90fa"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:678c67d60b35eced29777fe9398b6e6a6638156f143c80662a0c7c99ce115be7"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:9830af5aef2c17686d6e7c78c20b92c7b57c9d7921a03e4c549b48fe0e98c5c0"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:08651e17c868a6a22124b6ab71e939a5bb4737e0535f381ce35077dc8116c4b3"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-musllinux_1_2_s390x.whl", hash = "sha256:b58b4c944d7ce20cd7b426ae8834433b5b1152391960960b778b37803f0ffc1c"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:2b38818468ddb0c8fc4b172031d65ced3be22ba82360c45909a0546b2857d3e4"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-win32.whl", hash = "sha256:91d1f2284d13b68f213d05b51cd6242f4cfa065d291af6f353f9cbedd28d8c0d"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:a5c724f95366ba7a2895147b0690609b6774384fa8621aa46a66cf332e4b612f"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e37b33263fad4accdba45c8566566d45fc01f47fd4afa3e265df9e0e3581d4f4"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f1e29495bc968e37352c315d23a38462d7e77fcfa1597d005d17ed93f9f3103"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad8a774f545eb5471587e0389fca4f855f36d58901c65547796d59fc13aee458"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75a5a0251e4ceca127b26d18f0965b7f3c820a2dd2c51c25015c819300fd5859"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:853e40535f0f57e8b605e1cbce1399c96bcd5ab99e60992d2c7669c689d0cbe5"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e04e1dd62d019cde936d18fcd21361f6c4695e0e73fd6dc509c4ccd9446d26d"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:2df377e3d04f5c427c9f79ef95bdf0b982bde76c1dbd4441f83268f3f1993a53"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:5f3cb8db19e7462ccb2e34b56feaccb2aac675ad8f77e28f8222b3e7c47d1f92"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:b40860b3e6d6108473836a29d3600e1b335192637e16e9421b43b58147ced3c1"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:ca4fd9e8b669e81bb74479bde61ee475d7a6832d667e2ce80e6136ddd7a0fedd"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:771e74a22f48c40b6402d0ca1d569ced5a796e118d4472da388744b5aa0ebd3f"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-win32.whl", hash = "sha256:4589bfca18ecf787598262327f7329fe1f4fc2655c04899d84451562e2099a57"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:f646ab958a601bad8925a876f5aa68bdf0ec3584630143ed1ad8e9df4e447044"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dfb8106a05af1cb1eadeea996171b52c80f18851972e49ffe91539e4fc064b0f"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b8b9b77a898b721fc634858fc43552119d3d303485adc6f28f3e76f028d5ea04"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c65efab1a0ab14f75314039694ac35d3181a5c8cf43584bd537b36caf2a6ccf9"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b450eee7e201c48aae58e2d45ef5d309a19cd49952cfb58d546fefbeef0a100"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8f0997202d7628c4312f0398122bdc5ada7fa79939d248652af40d9da689ef8"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dae69bef7628236d426e408fb14a40f0027bac1658a06efd29549b26ba369372"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef48e874bcc3ebf623672ec99f9aaa7b8a4f62fb270e33dad6db3739ea111086"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9006dc1a73e2b2a53421aa72decbcff08cb109f67a20f7d15a64ab140e0a1d2"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:96c07a345740fa71c0d8fc5fa7ea182ee24f62ebbf33d4d10c8c72d866dc332d"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:85a78af6f6e782e0df36f93c9e7c2dd568204f60a2ea55025c21d1837dea95ec"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:19eadc55bf69f9e9799a808cdcfc6657cf30511cb32849235d555adfa048a99f"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:377e8ad3bb3c17c43f860b570fd15e048246ade92babc9b310f2c417500aca57"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f8e07aec529d6fa31516201c524b0cfac108a9a6044a148f236291aae7991195"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-win32.whl", hash = "sha256:703ab55b77b1c1ebb80eb0b27574a8eadf109739d252de7f646dc41cb82f1a65"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp38-cp38-win_amd64.whl", hash = "sha256:b4f4b2e019c6d1ad33d5fc3163d31d0f731a073a5a52cdfae7b85408548ce342"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a84e3b098a29b8c298b01291cf8bc850a507ca45507d43674a84a8d33b7595b2"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9905b580cfdbd6945e44d81332483deace167d33e956ffae5c4b27eddeb676e7"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e0c403a7418631dc7185ef8053acc765101f4f64cc0bf50d1bc44ae7d40fc28e"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d63f89bf28de4ce82a7c324275ce733bf31eb29ec1121e48261af89b5b7f30b"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc6dc782f5be4883279079c79fa88578258a0fd24651f6d69b0f4be2716f7d7e"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743c177822c60e66c5c9579b4f384bd98e60fd4a2abe0eacdec0af4747d925bc"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ec5eeb87220e4d2abf6faad1ecb3b3ee88c4d9caad6cf2ce4c0a73a91c4c7ad9"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9318b814363b4bc062e54852ea62f58b69e7da9e51211afd6c55e9170e1ae9a0"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e7a1f58a2614f2ad9fcb4822f6da56313cbb88309880512bf5d01bd3d9142b87"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ae9427ddde670f605c84f704c12d840628467cc0f0a8e9ce6489577eef6a0479"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:be75ae311433254af3c6fe6eb68bf80ac6ef547ec0cf4594f5b301a044682186"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:73e1438437bbe67d453e2908b90b17b357a992a9ac0011ad20af1ea7c2b4cd58"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:393c0a9af080c1c1a0801cca9448eff3633dafc1a7934fdce58a8d1c15d8bd2b"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-win32.whl", hash = "sha256:13a79fc8e9b69bf6d70e7fa5a53bd42fab83dc3e6e93da7fa82871ec40874e43"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-win_amd64.whl", hash = "sha256:b1648708e5bf599b4455bf330faa4777c3963669acb2f3fa25240123a01f8b2b"},
{file = "sqlcipher3_wheels-0.5.5.post0-cp39-cp39-win_arm64.whl", hash = "sha256:4c3dd2f54bdd518b90e28b07c31cdfe34c8bd182c5107a30a9c2ef9569cd6cf9"},
{file = "sqlcipher3_wheels-0.5.5.post0.tar.gz", hash = "sha256:2c291ba05fa3e57c9b4d407d2751aa69266b5372468e7402daaa312b251aca7f"},
{file = "sqlcipher3_wheels-0.5.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e16c8caf59e86589fb5f52253420db07121f1f96e2a12e244f6fdcaf8b946530"},
{file = "sqlcipher3_wheels-0.5.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:337f2e059114729dd1529ee356c98e2aa06440d6a9772917514a3bda0647c61c"},
{file = "sqlcipher3_wheels-0.5.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f6bd900658446e1cdeebda0760adb9a89f55888b460623db88b100845cb51bc2"},
{file = "sqlcipher3_wheels-0.5.6-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:dc6fcca569858145cb5ba3c878997d1788973e36f689090178f807b9a44d9ca6"},
{file = "sqlcipher3_wheels-0.5.6-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:eef50cc39554ad1fb82faa33d25c7f3cb11e2f7087b41109bc169db2c942f0c7"},
{file = "sqlcipher3_wheels-0.5.6-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:0fc36fc67f639a0e03cf6f7c6a5d1bc5cdd8005e8e07da3b21c54d4d81ed353b"},
{file = "sqlcipher3_wheels-0.5.6-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:53d0b861668d6847c7cc0dc7b443263b95a5cd211bcc326a457bd3122ebbb5a0"},
{file = "sqlcipher3_wheels-0.5.6-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:10aef293397a4ab25d8346ba5f96181214ab9c6a8836d83320cf23a2ad773a2c"},
{file = "sqlcipher3_wheels-0.5.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1105e7edba36a29625a824bff0eca3685c1cf6e391182b85a9a73b4b1604eef3"},
{file = "sqlcipher3_wheels-0.5.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5db9b4035e42a27672abbe75120908c74a235a496cd92b4c685fda1e95e9b19c"},
{file = "sqlcipher3_wheels-0.5.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f9e3fb5e96c5067a8cfd7b2fa7d939e529e30439058bbc15d0e9adca5e4cff1b"},
{file = "sqlcipher3_wheels-0.5.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6f3c1a8a4a2c04225f5159cf7f1c315101a89271afbaef4205c6fc50766c5535"},
{file = "sqlcipher3_wheels-0.5.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fc0504a1dbe6d478614ef55eb80d0c02ead24bc91f34b41c07d404452389f42d"},
{file = "sqlcipher3_wheels-0.5.6-cp310-cp310-win32.whl", hash = "sha256:05ef2b35f176e3b29092ec9aa03b09f4803feddbabdc2174e7ccc608758f2beb"},
{file = "sqlcipher3_wheels-0.5.6-cp310-cp310-win_amd64.whl", hash = "sha256:0f6873e4badf64eb8c5771c9e8a726df46ac663bc8051dfefb51fe2a46358b37"},
{file = "sqlcipher3_wheels-0.5.6-cp310-cp310-win_arm64.whl", hash = "sha256:9fd30c1cffa10f63f504a33494564efc0e0a475bbf069487016a9d2462d115e5"},
{file = "sqlcipher3_wheels-0.5.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a6c511bacd40ba769368b1abbf97fbefb285f525e6d2a399a704c22ba2aae37f"},
{file = "sqlcipher3_wheels-0.5.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa25610cda2b2a1b1cefddbd93488e939cf0059480f2fda5a8704acddd0e8935"},
{file = "sqlcipher3_wheels-0.5.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5a5258fb99e99b6fda6f011a0a4094ff99fe2e9b9ac7ce81cf646e0e779829a3"},
{file = "sqlcipher3_wheels-0.5.6-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:459836d52904fa006bf36e2144959bd21577c32947fdd173db50b037108a8620"},
{file = "sqlcipher3_wheels-0.5.6-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:5b36f9949f4d35c72f0626aaac109b17688c1d6a9a6e11de2538b4cfc32cfad0"},
{file = "sqlcipher3_wheels-0.5.6-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:87301b545556a1811780bb6fc6480ab1f2640d1d5b5e5e33ed404559ae383647"},
{file = "sqlcipher3_wheels-0.5.6-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:fcc4705b5b7bd3508d08a6389a45e14591071a3e575c2864c9c1c615df89e0da"},
{file = "sqlcipher3_wheels-0.5.6-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:0a231eb677a8246c47e423c710198631850c0a090e8f02a7fb1ad266ba517c56"},
{file = "sqlcipher3_wheels-0.5.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:71ef871c65ad7c61048acb4f57da29bc0d5e35874183006222c229b5f1f64c73"},
{file = "sqlcipher3_wheels-0.5.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c3480298c9bc4117207535636fe74b01b4860ecd74a028c73b42f5f0ddaa8661"},
{file = "sqlcipher3_wheels-0.5.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d48cf218ed13f17e3037564f08fba7ddf2c260dac7993e3d4ac58ee30483f115"},
{file = "sqlcipher3_wheels-0.5.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ff57a80904b9bd55e18774cb59bffacad06e196298381ee576ce683d1c09b032"},
{file = "sqlcipher3_wheels-0.5.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:50978685717cd9293ff5508c192695a894879f9faed5142d0e8d7b63310f87c2"},
{file = "sqlcipher3_wheels-0.5.6-cp311-cp311-win32.whl", hash = "sha256:24207dbb699ca68fc5fc7248385fdf33a92fb1e17a6ea88d3cf2345a18fb29ff"},
{file = "sqlcipher3_wheels-0.5.6-cp311-cp311-win_amd64.whl", hash = "sha256:40b1f8188a0aa5bbec354a12561b014b43a6a0d0a0d230a8a9378ed7b826b0ec"},
{file = "sqlcipher3_wheels-0.5.6-cp311-cp311-win_arm64.whl", hash = "sha256:107ef02bbd0f2ffb39a564c14ebf3bedfa4569949a0d72ec8e106f754d715b7c"},
{file = "sqlcipher3_wheels-0.5.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:59a572b18d1ef8318e9f583a7b3e1a67b4b04ed4b783c3f29fa806635274d12a"},
{file = "sqlcipher3_wheels-0.5.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32dfb8903b24db5879b1f922114f650bc6a15df9d071c55eefeb6937e13b2d20"},
{file = "sqlcipher3_wheels-0.5.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f5770257736c43cbf910a22f74c1490ef1ecde0432e475904f038e64ffdacb0"},
{file = "sqlcipher3_wheels-0.5.6-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c33f99ddfe08c0f34807046800e510316b8bac2974b3c5fb9ecb1ee25c391ac8"},
{file = "sqlcipher3_wheels-0.5.6-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:97d4c000deeb72c2421f555f3e55a8c161ddfb0499caabf60df2bfde6460a5fc"},
{file = "sqlcipher3_wheels-0.5.6-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:67d9889028b4adfcaecd32e1e60330e1764c209ad12438f0eec2a5145ebf4a2d"},
{file = "sqlcipher3_wheels-0.5.6-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:00cf178b15da486ab43ee2bed41edb1b393c5cfe2a48cae68893a2b31260dbd3"},
{file = "sqlcipher3_wheels-0.5.6-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:95bfa4c5ffdd72d9d8676c913d585b7885a42824824cf1d9e93d3669f01492dd"},
{file = "sqlcipher3_wheels-0.5.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:030ab50a8f4153cfe8dd5c98724909b210243af2350b9c79914838905a99518e"},
{file = "sqlcipher3_wheels-0.5.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5dc3c3d9deea654f8ea9c1dbc7bc90561331e4da9c7055381fac6498ca7267a3"},
{file = "sqlcipher3_wheels-0.5.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8cc986e8aa89e5a4a30b4eb8fd841d913a4e22ada99ec42be83f69bde3d86a31"},
{file = "sqlcipher3_wheels-0.5.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a41f0d30fa63d8db915566ec6987e68f064d96052cd6492ed8384b3e4807e60b"},
{file = "sqlcipher3_wheels-0.5.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f32fefe8a41e68334c545465813782fd45ef5cfe1082d012d95514c8a78e8015"},
{file = "sqlcipher3_wheels-0.5.6-cp312-cp312-win32.whl", hash = "sha256:ac2332f44758794a2fa19c77b824853e2a57ce5c27cc71c61066a52845be22d0"},
{file = "sqlcipher3_wheels-0.5.6-cp312-cp312-win_amd64.whl", hash = "sha256:6f016ba5a2a531938f332a234865dfc25d3a69abc169c3bf1d5c06c3c3f24601"},
{file = "sqlcipher3_wheels-0.5.6-cp312-cp312-win_arm64.whl", hash = "sha256:101ce0f7403801b6988d1f6c94244900e0f6c5378666e0ffd74b300687a6f9ef"},
{file = "sqlcipher3_wheels-0.5.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:94527fa3994c0fa1275c23d9fbb02512aacc675f1e45f566c660f4f9d5376e75"},
{file = "sqlcipher3_wheels-0.5.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0920a4b24362522ba83b36a47495d174221361213207191c325749a621fabeca"},
{file = "sqlcipher3_wheels-0.5.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5061b07b121ebd76aa697755b1b8f642cc3a27a0f6d392180ab249b35f1c2394"},
{file = "sqlcipher3_wheels-0.5.6-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:79de8511bb1fec62128e1b366cdc0cbd2ad1d725f3e29f9c91e96946a3c67945"},
{file = "sqlcipher3_wheels-0.5.6-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:4b92c2f35bb8153cc20bcfc651536f51cc1194403782c542a852497ac789cbe2"},
{file = "sqlcipher3_wheels-0.5.6-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2d55211e3d2addff8a2df7335927d7fe6d75aa9ed12b396a22a5a0bfe2773ed9"},
{file = "sqlcipher3_wheels-0.5.6-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:8cb31de5d67799cc2bba92f23adc10281d66c2c16ca6418b94d80500a164aa60"},
{file = "sqlcipher3_wheels-0.5.6-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:123796de3e471db5ed8b4ee4f97ec562ad38347ad678dad71133eade280202e0"},
{file = "sqlcipher3_wheels-0.5.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b6d34fabacfad4f301a22b5d8466d7ee3481f735bdb327d8756f04c81d3516c4"},
{file = "sqlcipher3_wheels-0.5.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:91b02fc765485c5b65f2a3eacfd2e16059253e007d0b5a5f24bba5fcea9032dd"},
{file = "sqlcipher3_wheels-0.5.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:13db7f23c553ffdd35f6e3b26415bdb9f100dcf89038873965caef769e8f1af5"},
{file = "sqlcipher3_wheels-0.5.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4ba79a81cd591d32a3a225e3e9b50a9871324d0e414fb6d0866049d8820e4e46"},
{file = "sqlcipher3_wheels-0.5.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f97be07997681ca90fb339d5411fcb957bd7cbe810389404baed207cb366badd"},
{file = "sqlcipher3_wheels-0.5.6-cp313-cp313-win32.whl", hash = "sha256:9e56e0a7aa778da3d46323fc1233da5dcede795a6c7fe4c11980fec0ce8c3fe3"},
{file = "sqlcipher3_wheels-0.5.6-cp313-cp313-win_amd64.whl", hash = "sha256:744845e4aa3cc614590f967aa1d38cc5d549177a2a83ed68c1821b5fb0505f8a"},
{file = "sqlcipher3_wheels-0.5.6-cp313-cp313-win_arm64.whl", hash = "sha256:c92de0b940533ca3a5b43a45d0768e0698b6ca95020b2fd47ec269b6bfc228d1"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a3f558df797aabf51680b3fbce48c4b3df89c36ad7fcaa3886b2ed8057aa2786"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7e216586720663960c82f046c495ef6d828e8e95c8fcf4c767b555fb9b8feead"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3e4ef70d3af8ebe6ababe8eff93b8bd4ad288d0a38ab29a2420c91d636fbfe14"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:11e34aac6cb7e29d23e339c5de9e87700ddf09886e104640578b5afb566a2c50"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:79e220312a075546e6be0a6062dda6315857b1478d78f97eb352f1383dde8ce2"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314-manylinux_2_28_ppc64le.whl", hash = "sha256:b953af7b57867bcffeeab59681921671615ae4b42fd0a9234ad0be7e0e43dfd4"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314-manylinux_2_28_s390x.whl", hash = "sha256:130ac318dbcb3a51a4377b0bf3e450c6c21d508a8b00d2d9d4b3ee6a46ab3595"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5154c8022e58722987522ddce30f19fb69d6f8f6314959100d9f37c3dc5cba5b"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f91d1f5b7b927aa00a8d83724c58875d9d0e47bd81ca40445090ab521b5fa"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e1c140bfa6b0a7e08f414f2a9f8f529f7d8c4cfa8386ce588e6c747c4ccc6615"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:18fc56dfb32c6ce370d929897205027f78275c32446d6b1be712d462789ae8c2"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c03ec5e058fbf3fd94ecd8e0448834e8e7f46418eaec5fe5c7a0982c6e62c13f"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08756c1b25aebabb25a55dfe6f323876caea0c69511e34553807ae1d7ab843dd"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314-win32.whl", hash = "sha256:bdbc58d224d27c002aed8a6361b43f3651943ecbfac69cd2674bbe681cf83790"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314-win_amd64.whl", hash = "sha256:dcc313f4519922c1ec3406b010d53f700750c1cf5331b9633a3c8b196307e852"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314-win_arm64.whl", hash = "sha256:dc1f0c77cc0395680176913a1d634a4014a1ebf02e7a7b2ac03a180b44241842"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fc30e82d2b8f139ac1ab81a3b3d9a59da8e3ce3b1e753285727480667efd5417"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f11d1d2c41141dd95f7d45f03dbe9f69a6427463e69db50609d83c0cd29980b5"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:92beff11fd9683941de7b47b8fc280e834b135ba7966d139b0ce2159b551ebad"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3060403647df7d44844c2808a384e4c4cf4a2a1b65e509a8016aca971c08ad39"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:9380de7e8fc952f376c9dae9ba1cdbb6a24ff5e41fd8f3b3cf39f1e305ed3248"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:9a26be381b0fb1c8d4fcdfd48182c78217ae9458513e4fe51b5045d4f94d41cb"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314t-manylinux_2_28_s390x.whl", hash = "sha256:c3be08f8d81372a6d084062f969f88be0b942ac449b0ac01825b853c12705421"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c5bd4abbebc15f8a2a9a653500cd1abeb3aac13887fcc83de31ca40fce32e3a2"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4bb3c2c8e9a1e16455b989b2c7598b8053029bcbb519dc22601fa82bc8896f89"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:aac8ca9d2b4e18637e61ea1d8193500a1186f0b113b9224dc74186190f41c8e7"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f237a41c3f08e69f2532aec29a2589097baa73886164537d90c744d3d2eb3b3"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:e6e59c3e0301cb04351b1cb12231aaadb40f56f779fb50a7857c6b4ed4c57297"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba2296a608081f4474f4447658a1e032d0b5506153baf68233471afde1463da9"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314t-win32.whl", hash = "sha256:8c8edfbd38a49ebbec2d1d56a000a499da2ac80b00488c156a1e0b8a7b8c10c6"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314t-win_amd64.whl", hash = "sha256:21df85bc14d5d86225c1e7466ff65cbcc10f0d1d4f466823b4534c4c0564554c"},
{file = "sqlcipher3_wheels-0.5.6-cp314-cp314t-win_arm64.whl", hash = "sha256:64df3e807fb0e6d89c1e90ce7c900bb82b695c474e1a0945a5f92862cac8b63d"},
{file = "sqlcipher3_wheels-0.5.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3784b22a29e4a675b456ca6ff1841d61e0eb97a28d0ba23d3d8cb5fe6da88238"},
{file = "sqlcipher3_wheels-0.5.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e27881be24f03d8a67a6db763f5671aaa05205de2380b1793b5e20bdabe49fba"},
{file = "sqlcipher3_wheels-0.5.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:667b6eec50ed03111676a0f4565be133643c9ad8bc88e6eea1c96b2af590c417"},
{file = "sqlcipher3_wheels-0.5.6-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:4eaaa5cf77b125e05908b1200681e2988b1a6a307c7e677967053a1e4b07fba5"},
{file = "sqlcipher3_wheels-0.5.6-cp38-cp38-manylinux_2_28_i686.whl", hash = "sha256:4ead5b8f2607718548c8571e4a89fe735dd53443a2b5e42d8147eecd11b0d94b"},
{file = "sqlcipher3_wheels-0.5.6-cp38-cp38-manylinux_2_28_ppc64le.whl", hash = "sha256:d82a8a7b478d23368320ad185533d063ec14d11a1d188f07ace513a66bfa9580"},
{file = "sqlcipher3_wheels-0.5.6-cp38-cp38-manylinux_2_28_s390x.whl", hash = "sha256:39d871ee8c13d9b0326b24a02e5af21a7b1c8fb5e6f6f4ec62b935392202ec69"},
{file = "sqlcipher3_wheels-0.5.6-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:5a8737683621c2917a4ee9ff774e597a368c5b3d23f08ae53897d6bd1f8bfc0e"},
{file = "sqlcipher3_wheels-0.5.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:08b6922d5020384fa641c8dc416f6f2b143110c86dcf3aae086e7ce15b192eae"},
{file = "sqlcipher3_wheels-0.5.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f9dcc7f830ec56c090884a83be265c51c0a4fd60bb033b000c69c3bee08d77d8"},
{file = "sqlcipher3_wheels-0.5.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:0848f628b1528dd6a19a36679d8cde4b6f1f8d288757ba2e3df5578b79d79e90"},
{file = "sqlcipher3_wheels-0.5.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:1476bb15586ce27ea5fae7c54469b2be4efe51ca9cefa20871a6c394a18892cc"},
{file = "sqlcipher3_wheels-0.5.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:de17d373d9e7807236013950f598bf59b9ed7c375938fdb95378a7114e55ff95"},
{file = "sqlcipher3_wheels-0.5.6-cp38-cp38-win32.whl", hash = "sha256:02fa9e7f98a8e9be871219014b9ac015ba630b51615d90a2c06d45547a4b0cf1"},
{file = "sqlcipher3_wheels-0.5.6-cp38-cp38-win_amd64.whl", hash = "sha256:6b2d7daab225c578aec8109fde99624f281b4ccdc6c53c8cd8feb86d8e7d3cf2"},
{file = "sqlcipher3_wheels-0.5.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:abef5e28b4d1ca518291a8ca27af1cf9e4d68dd4a264d83874ec4d0a69589395"},
{file = "sqlcipher3_wheels-0.5.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cd4c12a5a60cbd533ba4a3b4131d23302283ba597739c7867066b4efefe178db"},
{file = "sqlcipher3_wheels-0.5.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b7672837f1b9a6a67e375b743d74371d0428ead79ff367591145d06f3711c96"},
{file = "sqlcipher3_wheels-0.5.6-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:61c33e2697b0d91f3cbe806104e1d5b93961d3ab55ba55ee53bb36efe83c9933"},
{file = "sqlcipher3_wheels-0.5.6-cp39-cp39-manylinux_2_28_i686.whl", hash = "sha256:2e6eb09782dd719a1bb34af6e5ef25e5713c1f806231b472fcf64eb9288957af"},
{file = "sqlcipher3_wheels-0.5.6-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:6469b756ced0293e74806db2f114e5307cd4b05a559e986d3cc0b2eeb1eb8153"},
{file = "sqlcipher3_wheels-0.5.6-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:b6492f9bcb9296ac2179b5c9f7e7f329449b580836c0e8e5cfc2f3fe9af3486c"},
{file = "sqlcipher3_wheels-0.5.6-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2e4968d98917309463f02e4a48abebd95ed3d37968346f2693ed8a08e2fe9794"},
{file = "sqlcipher3_wheels-0.5.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:50214729697a1ee9e7603ba62b8ea46d78903ae1332caaa94fbaedde113944b7"},
{file = "sqlcipher3_wheels-0.5.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1ec9fd1dd5774d665903b8ba2e3e4f8ed72879dc42f6e9b2815040f0cb2d8ccd"},
{file = "sqlcipher3_wheels-0.5.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ced8ab30d205c8b6225b5703885576e629266767b091158731ec76c8c490bef4"},
{file = "sqlcipher3_wheels-0.5.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:3c7242a267dd802fee273084a5707a95d02df4102afbea133c8f716234c7edcc"},
{file = "sqlcipher3_wheels-0.5.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7a6c239d15085af4b0f3433fa274c1fc37369509b99a7c035a359d5142a0536d"},
{file = "sqlcipher3_wheels-0.5.6-cp39-cp39-win32.whl", hash = "sha256:cc29963df04a73d8420a4d023ba016c9013d86378969d8a11fe2148477282936"},
{file = "sqlcipher3_wheels-0.5.6-cp39-cp39-win_amd64.whl", hash = "sha256:38cc7bb3a371c4a5fe7f4236a409e64f1286796d780833243f9e15ef852f159d"},
{file = "sqlcipher3_wheels-0.5.6-cp39-cp39-win_arm64.whl", hash = "sha256:186e49af3ddb98d260b95d436eaf58f2125712c268c8475627129c1f80a68164"},
{file = "sqlcipher3_wheels-0.5.6.tar.gz", hash = "sha256:1d232c14be44db95a7f3018433cae01ecd18803fa2468fce3cc45ebd5e034942"},
]
[[package]]

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "bouquin"
version = "0.5.5"
version = "0.6.0"
description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
authors = ["Miguel Jacq <mig@mig5.net>"]
readme = "README.md"

View file

@ -61,8 +61,10 @@ def test_dates_with_content_and_search(fresh_db):
assert _today() in dates and _yesterday() in dates and _tomorrow() in dates
hits = list(fresh_db.search_entries("alpha"))
assert any(d == _today() for d, _ in hits)
assert any(d == _tomorrow() for d, _ in hits)
# search_entries now returns (kind, key, title, text, aux)
page_dates = [key for (kind, key, _title, _text, _aux) in hits if kind == "page"]
assert _today() in page_dates
assert _tomorrow() in page_dates
def test_get_all_entries_and_export(fresh_db, tmp_path):

View file

@ -33,7 +33,10 @@ def test_open_selected_with_data(qtbot, fresh_db):
it = QListWidgetItem("dummy")
from PySide6.QtCore import Qt
it.setData(Qt.ItemDataRole.UserRole, "1999-12-31")
it.setData(
Qt.ItemDataRole.UserRole,
{"kind": "page", "date": "1999-12-31"},
)
s.results.addItem(it)
s._open_selected(it)
assert seen == ["1999-12-31"]
@ -95,6 +98,6 @@ def test_populate_results_shows_both_ellipses(qtbot, fresh_db):
qtbot.addWidget(s)
s.show()
long = "X" * 40 + "alpha" + "Y" * 40
rows = [("2000-01-01", long)]
rows = [("page", "2000-01-01", "2000-01-01", long, None)]
s._populate_results("alpha", rows)
assert s.results.count() >= 1