From 54f26c198d7aac96246d242187fe11a5dbcf82ee Mon Sep 17 00:00:00 2001 From: MikaKerman Date: Mon, 5 May 2025 12:43:01 +0300 Subject: [PATCH 1/3] Add FileSystemMessagingIntegration and related tests - Implemented FileSystemMessagingIntegration for sending messages to files. - Added unit tests for message sending, file creation, and handling of the create_if_missing flag. - Updated messaging integrations __init__.py to include the new integration. --- .../messaging_integrations/__init__.py | 9 +++ .../messaging_integrations/file_system.py | 72 +++++++++++++++++++ .../test_file_system.py | 61 ++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 elementary/messages/messaging_integrations/file_system.py create mode 100644 tests/unit/messages/messaging_integrations/test_file_system.py diff --git a/elementary/messages/messaging_integrations/__init__.py b/elementary/messages/messaging_integrations/__init__.py index e69de29bb..64556ee55 100644 --- a/elementary/messages/messaging_integrations/__init__.py +++ b/elementary/messages/messaging_integrations/__init__.py @@ -0,0 +1,9 @@ +from .file_system import FileSystemMessagingIntegration +from .slack_webhook import SlackWebhookMessagingIntegration +from .teams_webhook import TeamsWebhookMessagingIntegration + +__all__ = [ + "FileSystemMessagingIntegration", + "TeamsWebhookMessagingIntegration", + "SlackWebhookMessagingIntegration", +] diff --git a/elementary/messages/messaging_integrations/file_system.py b/elementary/messages/messaging_integrations/file_system.py new file mode 100644 index 000000000..377707914 --- /dev/null +++ b/elementary/messages/messaging_integrations/file_system.py @@ -0,0 +1,72 @@ +import os +from datetime import datetime + +from elementary.messages.message_body import MessageBody +from elementary.messages.messaging_integrations.base_messaging_integration import ( + BaseMessagingIntegration, + MessageSendResult, +) +from elementary.messages.messaging_integrations.empty_message_context import ( + EmptyMessageContext, +) +from elementary.messages.messaging_integrations.exceptions import ( + MessagingIntegrationError, +) +from elementary.utils.log import get_logger + +logger = get_logger(__name__) + + +class FileSystemMessagingIntegration( + BaseMessagingIntegration[str, EmptyMessageContext] +): + def __init__(self, directory: str, create_if_missing: bool = True) -> None: + self.directory = os.path.abspath(directory) + self._create_if_missing = create_if_missing + + if not os.path.exists(self.directory): + if self._create_if_missing: + logger.info( + "Creating directory for FileSystemMessagingIntegration: %s", + self.directory, + ) + os.makedirs(self.directory, exist_ok=True) + else: + raise MessagingIntegrationError( + f"Directory {self.directory} does not exist and create_if_missing is False" + ) + + def supports_reply(self) -> bool: + return False + + def send_message( + self, destination: str, body: MessageBody + ) -> MessageSendResult[EmptyMessageContext]: + file_path = os.path.join(self.directory, destination) + + if not os.path.exists(file_path) and not self._create_if_missing: + raise MessagingIntegrationError( + f"File {file_path} does not exist and create_if_missing is False" + ) + + try: + logger.info("Writing alert message to file %s", file_path) + with open(file_path, "a", encoding="utf-8") as fp: + fp.write(body.json()) + fp.write("\n") + except Exception as exc: + logger.error( + "Failed to write alert message to file %s: %s", + file_path, + exc, + exc_info=True, + ) + raise MessagingIntegrationError( + f"Failed writing alert message to file {file_path}" + ) from exc + + return MessageSendResult( + timestamp=datetime.utcnow(), + message_format="json", + message_context=EmptyMessageContext(), + ) diff --git a/tests/unit/messages/messaging_integrations/test_file_system.py b/tests/unit/messages/messaging_integrations/test_file_system.py new file mode 100644 index 000000000..9eef50739 --- /dev/null +++ b/tests/unit/messages/messaging_integrations/test_file_system.py @@ -0,0 +1,61 @@ +import json +from pathlib import Path + +import pytest + +from elementary.messages.blocks import LineBlock, LinesBlock, TextBlock +from elementary.messages.message_body import MessageBody +from elementary.messages.messaging_integrations.exceptions import ( + MessagingIntegrationError, +) +from elementary.messages.messaging_integrations.file_system import ( + FileSystemMessagingIntegration, +) + + +def _build_body() -> MessageBody: + return MessageBody( + blocks=[LinesBlock(lines=[LineBlock(inlines=[TextBlock(text="hello")])])] + ) + + +def test_send_message_creates_file_and_appends(tmp_path: Path) -> None: + directory = tmp_path / "alerts" + integ = FileSystemMessagingIntegration(directory=str(directory)) + body = _build_body() + + integ.send_message("channel.json", body) + + target_file = directory / "channel.json" + assert target_file.exists() + + with target_file.open() as fp: + lines = fp.readlines() + assert len(lines) == 1 + assert json.loads(lines[0]) == json.loads(body.json()) + + +def test_send_multiple_messages(tmp_path: Path) -> None: + directory = tmp_path / "alerts" + integ = FileSystemMessagingIntegration(directory=str(directory)) + body1 = _build_body() + body2 = _build_body() + + integ.send_message("channel.json", body1) + integ.send_message("channel.json", body2) + + target_file = directory / "channel.json" + with target_file.open() as fp: + lines = fp.readlines() + + assert len(lines) == 2 + assert json.loads(lines[0]) == json.loads(body1.json()) + assert json.loads(lines[1]) == json.loads(body2.json()) + + +def test_send_message_no_create_flag(tmp_path: Path) -> None: + directory = tmp_path / "alerts-no-create" + with pytest.raises(MessagingIntegrationError): + FileSystemMessagingIntegration( + directory=str(directory), create_if_missing=False + ) From fe8facf1dc442915d8040b9159e5e2f096cb0601 Mon Sep 17 00:00:00 2001 From: MikaKerman Date: Mon, 5 May 2025 14:48:44 +0300 Subject: [PATCH 2/3] Refactor FileSystemMessagingIntegration: use pathlib logs with f-strings and update tests to match new channel-directory JSON file behavior --- .../messaging_integrations/file_system.py | 35 ++++++++-------- .../test_file_system.py | 40 +++++++++---------- 2 files changed, 36 insertions(+), 39 deletions(-) diff --git a/elementary/messages/messaging_integrations/file_system.py b/elementary/messages/messaging_integrations/file_system.py index 377707914..dc7ef983d 100644 --- a/elementary/messages/messaging_integrations/file_system.py +++ b/elementary/messages/messaging_integrations/file_system.py @@ -1,5 +1,5 @@ -import os from datetime import datetime +from pathlib import Path from elementary.messages.message_body import MessageBody from elementary.messages.messaging_integrations.base_messaging_integration import ( @@ -21,16 +21,15 @@ class FileSystemMessagingIntegration( BaseMessagingIntegration[str, EmptyMessageContext] ): def __init__(self, directory: str, create_if_missing: bool = True) -> None: - self.directory = os.path.abspath(directory) + self.directory = Path(directory).expanduser().resolve() self._create_if_missing = create_if_missing - if not os.path.exists(self.directory): + if not self.directory.exists(): if self._create_if_missing: logger.info( - "Creating directory for FileSystemMessagingIntegration: %s", - self.directory, + f"Creating directory for FileSystemMessagingIntegration: {self.directory}" ) - os.makedirs(self.directory, exist_ok=True) + self.directory.mkdir(parents=True, exist_ok=True) else: raise MessagingIntegrationError( f"Directory {self.directory} does not exist and create_if_missing is False" @@ -42,23 +41,23 @@ def supports_reply(self) -> bool: def send_message( self, destination: str, body: MessageBody ) -> MessageSendResult[EmptyMessageContext]: - file_path = os.path.join(self.directory, destination) + channel_dir = self.directory / destination + if not channel_dir.exists(): + if self._create_if_missing: + channel_dir.mkdir(parents=True, exist_ok=True) + else: + raise MessagingIntegrationError( + f"Channel directory {channel_dir} does not exist and create_if_missing is False" + ) - if not os.path.exists(file_path) and not self._create_if_missing: - raise MessagingIntegrationError( - f"File {file_path} does not exist and create_if_missing is False" - ) + filename = datetime.utcnow().strftime("%Y%m%dT%H%M%S_%fZ.json") + file_path = channel_dir / filename try: - logger.info("Writing alert message to file %s", file_path) - with open(file_path, "a", encoding="utf-8") as fp: - fp.write(body.json()) - fp.write("\n") + file_path.write_text(body.json(), encoding="utf-8") except Exception as exc: logger.error( - "Failed to write alert message to file %s: %s", - file_path, - exc, + f"Failed to write alert message to file {file_path}: {exc}", exc_info=True, ) raise MessagingIntegrationError( diff --git a/tests/unit/messages/messaging_integrations/test_file_system.py b/tests/unit/messages/messaging_integrations/test_file_system.py index 9eef50739..185a763a3 100644 --- a/tests/unit/messages/messaging_integrations/test_file_system.py +++ b/tests/unit/messages/messaging_integrations/test_file_system.py @@ -19,38 +19,36 @@ def _build_body() -> MessageBody: ) -def test_send_message_creates_file_and_appends(tmp_path: Path) -> None: - directory = tmp_path / "alerts" - integ = FileSystemMessagingIntegration(directory=str(directory)) +def test_send_message_creates_file_in_channel_dir(tmp_path: Path) -> None: + root_dir = tmp_path / "alerts" + integ = FileSystemMessagingIntegration(directory=str(root_dir)) body = _build_body() - integ.send_message("channel.json", body) + integ.send_message("channel", body) - target_file = directory / "channel.json" - assert target_file.exists() + channel_dir = root_dir / "channel" + files = list(channel_dir.glob("*.json")) + assert len(files) == 1, "Expected exactly one file in the channel directory" - with target_file.open() as fp: - lines = fp.readlines() - assert len(lines) == 1 - assert json.loads(lines[0]) == json.loads(body.json()) + message_json = files[0].read_text() + assert json.loads(message_json) == json.loads(body.json()) -def test_send_multiple_messages(tmp_path: Path) -> None: - directory = tmp_path / "alerts" - integ = FileSystemMessagingIntegration(directory=str(directory)) +def test_send_multiple_messages_creates_multiple_files(tmp_path: Path) -> None: + root_dir = tmp_path / "alerts" + integ = FileSystemMessagingIntegration(directory=str(root_dir)) body1 = _build_body() body2 = _build_body() - integ.send_message("channel.json", body1) - integ.send_message("channel.json", body2) + integ.send_message("channel", body1) + integ.send_message("channel", body2) - target_file = directory / "channel.json" - with target_file.open() as fp: - lines = fp.readlines() + channel_dir = root_dir / "channel" + files = sorted(channel_dir.glob("*.json")) - assert len(lines) == 2 - assert json.loads(lines[0]) == json.loads(body1.json()) - assert json.loads(lines[1]) == json.loads(body2.json()) + assert len(files) == 2 + assert json.loads(files[0].read_text()) == json.loads(body1.json()) + assert json.loads(files[1].read_text()) == json.loads(body2.json()) def test_send_message_no_create_flag(tmp_path: Path) -> None: From 1099dd6463acf5c729a20a927bc468974c3690c3 Mon Sep 17 00:00:00 2001 From: MikaKerman Date: Mon, 5 May 2025 15:21:11 +0300 Subject: [PATCH 3/3] style: isort imports for file_system --- elementary/messages/messaging_integrations/file_system.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/elementary/messages/messaging_integrations/file_system.py b/elementary/messages/messaging_integrations/file_system.py index dc7ef983d..173f3e9b5 100644 --- a/elementary/messages/messaging_integrations/file_system.py +++ b/elementary/messages/messaging_integrations/file_system.py @@ -1,3 +1,4 @@ +import json from datetime import datetime from pathlib import Path @@ -54,7 +55,8 @@ def send_message( file_path = channel_dir / filename try: - file_path.write_text(body.json(), encoding="utf-8") + json_str = json.dumps(body.dict(), indent=2) + file_path.write_text(json_str, encoding="utf-8") except Exception as exc: logger.error( f"Failed to write alert message to file {file_path}: {exc}",