From c7ce5b822c84a435b8381d87d1b893573232bb57 Mon Sep 17 00:00:00 2001 From: Siddhant Sharma Date: Sun, 5 Apr 2026 20:09:51 +0530 Subject: [PATCH 1/3] enh: add a delete chat service and clean up services and storage modules. --- src/memorytext/app.py | 4 +- src/memorytext/config.py | 6 +++ src/memorytext/db.py | 22 --------- src/memorytext/delegates/message_list.py | 4 +- src/memorytext/io/import_wa.py | 6 +-- src/memorytext/models/core.py | 10 ++-- src/memorytext/services/chat_service.py | 53 ++++++++++++---------- src/memorytext/services/message_service.py | 26 ++--------- src/memorytext/storage/chats_repo.py | 47 +++++++++++++++++++ src/memorytext/storage/db.py | 49 ++++++++++++++++++++ src/memorytext/storage/message_repo.py | 27 +++++++++++ 11 files changed, 175 insertions(+), 79 deletions(-) create mode 100644 src/memorytext/config.py delete mode 100644 src/memorytext/db.py create mode 100644 src/memorytext/storage/chats_repo.py create mode 100644 src/memorytext/storage/db.py create mode 100644 src/memorytext/storage/message_repo.py diff --git a/src/memorytext/app.py b/src/memorytext/app.py index 3a9dc65..3bd556b 100644 --- a/src/memorytext/app.py +++ b/src/memorytext/app.py @@ -4,14 +4,14 @@ QApplication, ) from memorytext.windows.main_window import MainWindow -from memorytext.db import init_db +from memorytext.storage.db import startup APP_NAME = "memory.text" def main(): # Pass in sys.argv to allow command line arguments for your app. - init_db() + startup() app = QApplication(sys.argv) window = MainWindow() window.show() # IMPORTANT!!!!! Windows are hidden by default. diff --git a/src/memorytext/config.py b/src/memorytext/config.py new file mode 100644 index 0000000..958a232 --- /dev/null +++ b/src/memorytext/config.py @@ -0,0 +1,6 @@ +from platformdirs import user_data_path + +APP_NAME = "memorytext" +USER_DIR = user_data_path(appname=APP_NAME, appauthor=False, ensure_exists=True) +DB_PATH = USER_DIR / f"{APP_NAME}-db.sqlite" +DB_URL = f"sqlite:///{DB_PATH}" diff --git a/src/memorytext/db.py b/src/memorytext/db.py deleted file mode 100644 index 26a29dd..0000000 --- a/src/memorytext/db.py +++ /dev/null @@ -1,22 +0,0 @@ -from __future__ import annotations -from typing import TYPE_CHECKING -from sqlalchemy import create_engine -from platformdirs import user_data_path -from memorytext.models.core import Base - -if TYPE_CHECKING: - from sqlalchemy import Engine - -BASE_DIR = user_data_path(appname="memorytext", appauthor=False, ensure_exists=True) - - -def init_db(): - engine: Engine - engine = create_engine(f"sqlite:///{BASE_DIR}/db.sqlite", echo=True) - Base.metadata.create_all(engine) - - -def get_db(): - engine: Engine - engine = create_engine(f"sqlite:///{BASE_DIR}/db.sqlite", echo=True) - return engine diff --git a/src/memorytext/delegates/message_list.py b/src/memorytext/delegates/message_list.py index 12fa357..2c14730 100644 --- a/src/memorytext/delegates/message_list.py +++ b/src/memorytext/delegates/message_list.py @@ -2,7 +2,7 @@ from PySide6.QtCore import Qt, QRect, QSize from PySide6.QtGui import QFont, QFontMetrics, QColor, QPixmap from memorytext.models.message_list import MessageList -from memorytext.db import BASE_DIR +from memorytext.config import USER_DIR USER_COLOR = "#ABE7FF" OTHER_COLOR = "#e0e0e0" @@ -35,7 +35,7 @@ def paint(self, painter, option, index): path = None if attachment and attachment.startswith("STK"): - path = str(BASE_DIR / "media" / attachment) + path = str(USER_DIR / "media" / attachment) extra_text = attachment + " (file attached)" text = text.replace(extra_text, "") diff --git a/src/memorytext/io/import_wa.py b/src/memorytext/io/import_wa.py index 09057de..8078470 100644 --- a/src/memorytext/io/import_wa.py +++ b/src/memorytext/io/import_wa.py @@ -1,8 +1,7 @@ from __future__ import annotations from typing import TYPE_CHECKING -from sqlalchemy.orm import Session from sqlalchemy.exc import SQLAlchemyError -from memorytext.db import get_db +from memorytext.storage.db import get_db_session from memorytext.models.core import Conversation if TYPE_CHECKING: @@ -13,8 +12,7 @@ def import_whatsapp( path: str | Path, title: str, tz: str = "Etc/UTC", username: str | None = None ): conversation = Conversation.from_whatsapp(path, title, tz, username) - engine = get_db() - with Session(engine) as session: + with get_db_session() as session: try: with session.begin(): session.add(conversation) diff --git a/src/memorytext/models/core.py b/src/memorytext/models/core.py index f726277..01bc331 100644 --- a/src/memorytext/models/core.py +++ b/src/memorytext/models/core.py @@ -30,8 +30,10 @@ class Base(DeclarativeBase): message_tags = Table( "message_tags", Base.metadata, - Column("tag_id", ForeignKey("tag.id"), primary_key=True), - Column("message_id", ForeignKey("message.id"), primary_key=True), + Column("tag_id", ForeignKey("tag.id", ondelete="CASCADE"), primary_key=True), + Column( + "message_id", ForeignKey("message.id", ondelete="CASCADE"), primary_key=True + ), ) @@ -95,7 +97,9 @@ class Message(Base): tags: Mapped[List["Tag"]] = relationship( secondary=message_tags, back_populates="messages" ) - conversation_id: Mapped[int] = mapped_column(ForeignKey("conversation.id")) + conversation_id: Mapped[int] = mapped_column( + ForeignKey("conversation.id", ondelete="CASCADE") + ) conversation: Mapped["Conversation"] = relationship(back_populates="messages") def __str__(self) -> str: diff --git a/src/memorytext/services/chat_service.py b/src/memorytext/services/chat_service.py index 9b3916a..31b334b 100644 --- a/src/memorytext/services/chat_service.py +++ b/src/memorytext/services/chat_service.py @@ -1,38 +1,24 @@ from __future__ import annotations -from sqlalchemy.orm import Session, selectinload +from sqlalchemy.exc import SQLAlchemyError from sqlalchemy import select, update from memorytext.models.core import Conversation -from memorytext.db import get_db +from memorytext.storage.db import get_db_session +import memorytext.storage.chats_repo as chrepo def get_chats(count: int = 10): - engine = get_db() - with Session(engine) as session: - stmt = select(Conversation).options(selectinload(Conversation.messages)) - conversations = session.scalars(stmt).fetchmany(count) - chat_list = [(c.id, c.title, c.participants) for c in conversations] - return chat_list + with get_db_session() as session: + return chrepo.get_chats(session, limit=count) def get_participants(title: str): - engine = get_db() - with Session(engine) as session: - stmt = ( - select(Conversation) - .where(Conversation.title == title) - .options(selectinload(Conversation.messages)) - ) - conversation = session.scalar(stmt) - if conversation: - participants = conversation.participants - return participants - return set() + with get_db_session() as session: + return chrepo.get_participants_by_title(session, title) def set_username(title: str, username: str): - engine = get_db() - with Session(engine) as session: + with get_db_session() as session: stmt = ( update(Conversation) .where(Conversation.title == title) @@ -47,8 +33,27 @@ def set_username(title: str, username: str): def get_username(conversation_id: int | None): - engine = get_db() - with Session(engine) as session: + with get_db_session() as session: stmt = select(Conversation.username).where(Conversation.id == conversation_id) username = session.scalar(stmt) return username + + +def delete_chat(conversation_id: int): + with get_db_session() as session: + try: + chrepo.delete_chat(session, conversation_id) + session.commit() + except SQLAlchemyError: + session.rollback() + raise + + +def delete_chat_by_title(title: str): + with get_db_session() as session: + try: + chrepo.delete_chat_by_title(session, title) + session.commit() + except SQLAlchemyError: + session.rollback() + raise diff --git a/src/memorytext/services/message_service.py b/src/memorytext/services/message_service.py index 598bbae..a76420c 100644 --- a/src/memorytext/services/message_service.py +++ b/src/memorytext/services/message_service.py @@ -1,28 +1,10 @@ from __future__ import annotations -from sqlalchemy.orm import Session -from sqlalchemy import select, func - -from memorytext.models.core import Message -from memorytext.db import get_db +import memorytext.storage.message_repo as mrepo +from memorytext.storage.db import get_db_session def get_messages( conversation_id: int | None, limit: int = 10, offset: int = 0 ) -> tuple[list, int]: - engine = get_db() - with Session(engine) as session: - stmt = select(func.count(Message.id)).where( - Message.conversation_id == conversation_id - ) - total = session.scalar(stmt) or 0 - - stmt_2 = ( - select(Message) - .where(Message.conversation_id == conversation_id) - .order_by(Message.timestamp.asc(), Message.id.asc()) - .limit(limit) - .offset(offset) - ) - message_list = list(session.scalars(stmt_2).fetchall()) - session.expunge_all() - return message_list, total + with get_db_session() as session: + return mrepo.get_messages(session, conversation_id, limit, offset) diff --git a/src/memorytext/storage/chats_repo.py b/src/memorytext/storage/chats_repo.py new file mode 100644 index 0000000..3e82c63 --- /dev/null +++ b/src/memorytext/storage/chats_repo.py @@ -0,0 +1,47 @@ +from __future__ import annotations +from typing import TYPE_CHECKING +from sqlalchemy import delete, select +from sqlalchemy.orm import selectinload + +from memorytext.models.core import Conversation + +if TYPE_CHECKING: + from sqlalchemy.orm import Session + + +def get_chats(session: Session, limit: int = 10): + stmt = select(Conversation).limit(limit) + conversations = session.scalars(stmt).fetchall() + chat_list = [(c.id, c.title, c.participants) for c in conversations] + return chat_list + + +def get_participants_by_id(session: Session, conversation_id: int | None): + stmt = ( + select(Conversation) + .where(Conversation.id == conversation_id) + .options(selectinload(Conversation.messages)) + ) + conversation = session.scalar(stmt) + if conversation: + participants = conversation.participants + return participants + return set() + + +def get_participants_by_title(session: Session, title: str): + conversation_id = session.scalar( + select(Conversation.id).where(Conversation.title == title) + ) + return get_participants_by_id(session, conversation_id) + + +def delete_chat(session: Session, conversation_id: int | None): + session.execute(delete(Conversation).where(Conversation.id == conversation_id)) + + +def delete_chat_by_title(session: Session, title: str): + conversation_id = session.scalar( + select(Conversation.id).where(Conversation.title == title) + ) + delete_chat(session, conversation_id) diff --git a/src/memorytext/storage/db.py b/src/memorytext/storage/db.py new file mode 100644 index 0000000..9cad7c4 --- /dev/null +++ b/src/memorytext/storage/db.py @@ -0,0 +1,49 @@ +from __future__ import annotations +from typing import TYPE_CHECKING +import logging +from contextlib import contextmanager +from sqlalchemy import create_engine, event +from sqlalchemy.orm import sessionmaker +from sqlite3 import Connection as SQLite3Connection + +from memorytext.config import DB_URL +from memorytext.models.core import Base + +if TYPE_CHECKING: + from sqlalchemy import Engine + +engine: Engine = create_engine(DB_URL) + +logging.basicConfig() +logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) + + +@event.listens_for(engine, "connect") +def _set_sqlite_pragma(dbapi_connection, connection_record): + if isinstance(dbapi_connection, SQLite3Connection): + cursor = dbapi_connection.cursor() + cursor.execute("PRAGMA foreign_keys=ON;") + cursor.close() + + +SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False) + + +def init_db(): + "Initialize database from models schema." + Base.metadata.create_all(engine) + + +def startup(): + "Application startup" + init_db() + + +@contextmanager +def get_db_session(): + "Get a database session context manager." + Session = SessionLocal() + try: + yield Session + finally: + Session.close() diff --git a/src/memorytext/storage/message_repo.py b/src/memorytext/storage/message_repo.py new file mode 100644 index 0000000..0b6cee6 --- /dev/null +++ b/src/memorytext/storage/message_repo.py @@ -0,0 +1,27 @@ +from __future__ import annotations +from typing import TYPE_CHECKING +from sqlalchemy import select, func + +from memorytext.models.core import Message + +if TYPE_CHECKING: + from sqlalchemy.orm import Session + + +def get_messages( + session: Session, conversation_id: int | None, limit: int = 10, offset: int = 0 +) -> tuple[list, int]: + stmt = select(func.count(Message.id)).where( + Message.conversation_id == conversation_id + ) + total = session.scalar(stmt) or 0 + stmt_2 = ( + select(Message) + .where(Message.conversation_id == conversation_id) + .order_by(Message.timestamp.asc(), Message.id.asc()) + .limit(limit) + .offset(offset) + ) + message_list = list(session.scalars(stmt_2).fetchall()) + session.expunge_all() + return message_list, total From e194a6540e8ef00a70efe141700c213259f509dd Mon Sep 17 00:00:00 2001 From: Siddhant Sharma Date: Sun, 5 Apr 2026 20:10:50 +0530 Subject: [PATCH 2/3] enh: reduce message bubbles max width. --- src/memorytext/delegates/message_list.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/memorytext/delegates/message_list.py b/src/memorytext/delegates/message_list.py index 2c14730..4443d5d 100644 --- a/src/memorytext/delegates/message_list.py +++ b/src/memorytext/delegates/message_list.py @@ -53,7 +53,7 @@ def paint(self, painter, option, index): ts_font = QFont(base_font) ts_font.setPointSize(base_font.pointSize() - 3) - max_width = option.rect.width() * 0.8 + max_width = option.rect.width() * 0.5 inner_width = max_width - 2 * self.padding metrics = QFontMetrics(base_font) @@ -166,7 +166,7 @@ def sizeHint(self, option, index): is_same_sender = index.data(MessageList.IsSameSenderRole) attachment = index.data(MessageList.AttachmentRole) - max_width = option.rect.width() * 0.8 + max_width = option.rect.width() * 0.5 inner_width = max_width - 2 * self.padding base_font = QFont(option.font) ts_font = QFont(base_font) From 83c4ec1607d0114a421587e4b2f943b53ef606d7 Mon Sep 17 00:00:00 2001 From: Siddhant Sharma Date: Mon, 6 Apr 2026 17:09:45 +0530 Subject: [PATCH 3/3] doc: update CHANGELOG --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9590dec..157fb68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +## [v0.6.0] + +## Added + +1. Add a common engine for the app. +2. Add storage modules to keep the services layer lighter. + +## Changed + +1. Use a session factory for getting db sessions. +2. Reduce the maximum width of message bubbles. + + +--- + ## [v0.5.0] ### Added