Compare commits

..

8 Commits

3 changed files with 74 additions and 24 deletions

View File

@ -9,7 +9,7 @@ from typing import TypeVar, Type, Optional, ClassVar, Any
from .message import Message, MessageFilter, MessageError from .message import Message, MessageFilter, MessageError
ChatInst = TypeVar('ChatInst', bound='Chat') ChatInst = TypeVar('ChatInst', bound='Chat')
ChatDirInst = TypeVar('ChatDirInst', bound='ChatDir') ChatDBInst = TypeVar('ChatDBInst', bound='ChatDB')
class ChatError(Exception): class ChatError(Exception):
@ -39,6 +39,29 @@ class Chat:
""" """
self.messages = [m for m in self.messages if m.match(mfilter)] self.messages = [m for m in self.messages if m.match(mfilter)]
def sort(self, reverse: bool = False) -> None:
"""
Sort the messages according to 'Message.msg_id()'.
"""
try:
# the message may not have an ID if it doesn't have a file_path
self.messages.sort(key=lambda m: m.msg_id(), reverse=reverse)
except MessageError:
pass
def clear(self) -> None:
"""
Delete all messages.
"""
self.messages = []
def add_msgs(self, msgs: list[Message]) -> None:
"""
Add new messages and sort them if possible.
"""
self.messages += msgs
self.sort()
def print(self, dump: bool = False) -> None: def print(self, dump: bool = False) -> None:
if dump: if dump:
pp(self) pp(self)
@ -58,7 +81,7 @@ class Chat:
@dataclass @dataclass
class ChatDir(Chat): class ChatDB(Chat):
""" """
A 'Chat' class that is bound to a given directory structure. Supports reading A 'Chat' class that is bound to a given directory structure. Supports reading
and writing messages from / to that structure. Such a structure consists of and writing messages from / to that structure. Such a structure consists of
@ -74,17 +97,19 @@ class ChatDir(Chat):
# a MessageFilter that all messages must match (if given) # a MessageFilter that all messages must match (if given)
mfilter: Optional[MessageFilter] = None mfilter: Optional[MessageFilter] = None
file_suffix: str = default_file_suffix file_suffix: str = default_file_suffix
# the glob pattern for all messages
glob: Optional[str] = None
# set containing all file names of the current messages # set containing all file names of the current messages
message_files: set[str] = field(default_factory=set) message_files: set[str] = field(default_factory=set, repr=False)
@classmethod @classmethod
def from_dir(cls: Type[ChatDirInst], def from_dir(cls: Type[ChatDBInst],
cache_path: pathlib.Path, cache_path: pathlib.Path,
db_path: pathlib.Path, db_path: pathlib.Path,
glob: Optional[str] = None, glob: Optional[str] = None,
mfilter: Optional[MessageFilter] = None) -> ChatDirInst: mfilter: Optional[MessageFilter] = None) -> ChatDBInst:
""" """
Create a 'ChatDir' instance from the given directory structure. Create a 'ChatDB' instance from the given directory structure.
Reads all messages from 'db_path' into the local message list. Reads all messages from 'db_path' into the local message list.
Parameters: Parameters:
* 'cache_path': path to the directory for temporary messages * 'cache_path': path to the directory for temporary messages
@ -106,19 +131,20 @@ class ChatDir(Chat):
message_files.add(file_path.name) message_files.add(file_path.name)
except MessageError as e: except MessageError as e:
print(f"Error processing message in '{file_path}': {str(e)}") print(f"Error processing message in '{file_path}': {str(e)}")
return cls(messages, cache_path, db_path, mfilter, cls.default_file_suffix, message_files) return cls(messages, cache_path, db_path, mfilter,
cls.default_file_suffix, glob, message_files)
@classmethod @classmethod
def from_messages(cls: Type[ChatDirInst], def from_messages(cls: Type[ChatDBInst],
cache_path: pathlib.Path, cache_path: pathlib.Path,
db_path: pathlib.Path, db_path: pathlib.Path,
messages: list[Message], messages: list[Message],
mfilter: Optional[MessageFilter]) -> ChatDirInst: mfilter: Optional[MessageFilter]) -> ChatDBInst:
""" """
Create a ChatDir instance from the given message list. Create a ChatDB instance from the given message list. Note that the next
Note that the next call to 'dump()' will write all files call to 'dump()' will write all files in order to synchronize the messages.
in order to synchronize the messages. 'update()' is not Similarly, 'update()' will read all messages, so you may end up with a lot
supported until after the first 'dump()'. of duplicates when using 'update()' first.
""" """
return cls(messages, cache_path, db_path, mfilter) return cls(messages, cache_path, db_path, mfilter)
@ -137,9 +163,10 @@ class ChatDir(Chat):
def dump(self, to_db: bool = False, force_all: bool = False) -> None: def dump(self, to_db: bool = False, force_all: bool = False) -> None:
""" """
Writes all messages to the 'cache_path' or 'db_path'. If a message has no file_path, Write all messages to 'cache_path' (or 'db_path' if 'to_db' is True). If a message
it will create a new one. By default, only messages that have not been written has no file_path, a new one will be created. By default, only messages that have
(or read) before will be dumped. Use 'force_all' to force writing all message files. not been written (or read) before will be dumped. Use 'force_all' to force writing
all message files.
""" """
for message in self.messages: for message in self.messages:
# skip messages that we have already written (or read) # skip messages that we have already written (or read)
@ -152,3 +179,26 @@ class ChatDir(Chat):
file_path = self.db_path / fname if to_db else self.cache_path / fname file_path = self.db_path / fname if to_db else self.cache_path / fname
self.set_next_fid(fid) self.set_next_fid(fid)
message.to_file(file_path) message.to_file(file_path)
def update(self, from_cache: bool = False, force_all: bool = False) -> None:
"""
Read new messages from 'db_path' (or 'cache_path' if 'from_cache' is true).
By default, only messages that have not been read (or written) before will
be read. Use 'force_all' to force reading all messages.
"""
if from_cache:
file_iter = self.cache_path.glob(self.glob) if self.glob else self.cache_path.iterdir()
else:
file_iter = self.cache_path.glob(self.glob) if self.glob else self.cache_path.iterdir()
for file_path in sorted(file_iter):
if file_path.is_file():
if file_path.name in self.message_files and not force_all:
continue
try:
message = Message.from_file(file_path, self.mfilter)
if message:
self.messages.append(message)
self.message_files.add(file_path.name)
except MessageError as e:
print(f"Error processing message in '{file_path}': {str(e)}")
self.sort()

View File

@ -409,10 +409,10 @@ class Message():
return False return False
return True return True
def file_id(self) -> str: def msg_id(self) -> str:
""" """
Returns an ID that is unique within the directory of this message. Returns an ID that is unique throughout all messages in the same (DB) directory.
Currently this is simply the file name. Currently this is the file name. The ID is also used for sorting messages.
""" """
if self.file_path: if self.file_path:
return self.file_path.name return self.file_path.name

View File

@ -577,7 +577,7 @@ This is an answer.
self.assertSetEqual(tags, {Tag('tag1'), Tag('tag2')}) self.assertSetEqual(tags, {Tag('tag1'), Tag('tag2')})
class MessageFileIDTxtTestCase(CmmTestCase): class MessageIDTestCase(CmmTestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.file = tempfile.NamedTemporaryFile(delete=False, suffix='.txt') self.file = tempfile.NamedTemporaryFile(delete=False, suffix='.txt')
self.file_path = pathlib.Path(self.file.name) self.file_path = pathlib.Path(self.file.name)
@ -589,9 +589,9 @@ class MessageFileIDTxtTestCase(CmmTestCase):
self.file.close() self.file.close()
self.file_path.unlink() self.file_path.unlink()
def test_file_id_txt(self) -> None: def test_msg_id_txt(self) -> None:
self.assertEqual(self.message.file_id(), self.file_path.name) self.assertEqual(self.message.msg_id(), self.file_path.name)
def test_file_id_txt_exception(self) -> None: def test_msg_id_txt_exception(self) -> None:
with self.assertRaises(MessageError): with self.assertRaises(MessageError):
self.message_no_file_path.file_id() self.message_no_file_path.msg_id()