From d4d77a8955ce4a86918830abb68158a3809801eb Mon Sep 17 00:00:00 2001 From: Nathan Nowack Date: Tue, 31 Jan 2023 23:57:32 -0600 Subject: [PATCH 1/4] add mattermost webhook notifblock --- src/prefect/blocks/notifications.py | 73 +++++++++++++++++++++++++++++ tests/blocks/test_notifications.py | 48 +++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/src/prefect/blocks/notifications.py b/src/prefect/blocks/notifications.py index 373ceae16606..2bfbbbdf794a 100644 --- a/src/prefect/blocks/notifications.py +++ b/src/prefect/blocks/notifications.py @@ -3,6 +3,7 @@ import apprise from apprise import Apprise, AppriseAsset, NotifyType +from apprise.plugins.NotifyMattermost import NotifyMattermost from apprise.plugins.NotifyOpsgenie import NotifyOpsgenie from apprise.plugins.NotifyPagerDuty import NotifyPagerDuty from apprise.plugins.NotifyTwilio import NotifyTwilio @@ -400,3 +401,75 @@ def block_initialization(self) -> None: ).url() ) self._start_apprise_client(url) + + +class MatterMostWebhook(AbstractAppriseNotificationBlock): + """ + Enables sending notifications via a provided MatterMost webhook. + See [Apprise notify_mattermost docs](https://github.com/caronc/apprise/wiki/Notify_mattermost) # noqa + + + Examples: + Load a saved MatterMost webhook and send a message: + ```python + from prefect.blocks.notifications import MatterMostWebhook + mattermost_webhook_block = MatterMostWebhook.load("BLOCK_NAME") + mattermost_webhook_block.notify("Hello from Prefect!") + ``` + """ + + _description = "Enables sending notifications via a provided MatterMost webhook." + _block_type_name = "MatterMost Webhook" + _block_type_slug = "mattermost-webhook" + _logo_url = "https://images.ctfassets.net/zscdif0zqppk/3mlbsJDAmK402ER1sf0zUF/a48ac43fa38f395dd5f56c6ed29f22bb/mattermost-logo-png-transparent.png?h=250" + _documentation_url = "https://docs.prefect.io/api-ref/prefect/blocks/notifications/#prefect.blocks.notifications.MatterMostWebhook" + + hostname: str = Field( + default=..., + description="The hostname of your MatterMost server.", + example="mattermost.example.com", + ) + + token: SecretStr = Field( + default=..., + description="The token associated with your MatterMost webhook.", + ) + + botname: Optional[str] = Field( + default=None, + description="The name of the bot that will send the message.", + ) + + channels: Optional[List[str]] = Field( + default=None, + description="The channel(s) you wish to notify.", + ) + + include_image: bool = Field( + default=True, + description="Whether to include the Apprise status image in the message.", + ) + + fullpath: Optional[str] = Field( + default=None, + description="The path of your MatterMost server.", + ) + + port: int = Field( + default=8065, + description="The port of your MatterMost server.", + ) + + def block_initialization(self) -> None: + url = SecretStr( + NotifyMattermost( + token=self.token.get_secret_value(), + fullpath=self.fullpath, + host=self.hostname, + botname=self.botname, + channels=self.channels, + include_image=self.include_image, + port=self.port, + ).url() + ) + self._start_apprise_client(url) diff --git a/tests/blocks/test_notifications.py b/tests/blocks/test_notifications.py index f5a876195616..82433f094bb6 100644 --- a/tests/blocks/test_notifications.py +++ b/tests/blocks/test_notifications.py @@ -8,6 +8,7 @@ import prefect from prefect.blocks.notifications import ( AppriseNotificationBlock, + MatterMostWebhook, OpsgenieWebhook, PagerDutyWebHook, PrefectNotifyType, @@ -89,6 +90,53 @@ def test_is_picklable(self, block_class: Type[AppriseNotificationBlock]): assert isinstance(unpickled, block_class) +class TestMatterMostWebhook: + async def test_notify_async(self): + with patch("apprise.Apprise", autospec=True) as AppriseMock: + reload_modules() + + apprise_instance_mock = AppriseMock.return_value + apprise_instance_mock.async_notify = AsyncMock() + + mm_block = MatterMostWebhook(hostname="example.com", token="token") + await mm_block.notify("test") + + AppriseMock.assert_called_once() + apprise_instance_mock.add.assert_called_once_with( + f"mmost://{mm_block.hostname}/{mm_block.token.get_secret_value()}/" + "?image=yes&format=text&overflow=upstream&rto=4.0&cto=4.0&verify=yes" + ) + apprise_instance_mock.async_notify.assert_awaited_once_with( + body="test", title=None, notify_type=PrefectNotifyType.DEFAULT + ) + + def test_notify_sync(self): + with patch("apprise.Apprise", autospec=True) as AppriseMock: + reload_modules() + + apprise_instance_mock = AppriseMock.return_value + apprise_instance_mock.async_notify = AsyncMock() + + mm_block = MatterMostWebhook(hostname="example.com", token="token") + mm_block.notify("test") + + AppriseMock.assert_called_once() + apprise_instance_mock.add.assert_called_once_with( + f"mmost://{mm_block.hostname}/{mm_block.token.get_secret_value()}/" + "?image=yes&format=text&overflow=upstream&rto=4.0&cto=4.0&verify=yes" + ) + apprise_instance_mock.async_notify.assert_called_once_with( + body="test", title=None, notify_type=PrefectNotifyType.DEFAULT + ) + + def test_is_picklable(self): + reload_modules() + block = MatterMostWebhook(token="token", hostname="example.com") + pickled = cloudpickle.dumps(block) + unpickled = cloudpickle.loads(pickled) + assert isinstance(unpickled, MatterMostWebhook) + + class TestOpsgenieWebhook: API_KEY = "api_key" From cf63adcc72ce98a86b854be8d0eb89829ddb8eaa Mon Sep 17 00:00:00 2001 From: Nathan Nowack Date: Wed, 1 Feb 2023 02:43:48 -0600 Subject: [PATCH 2/4] `MatterMost` to `Mattermost` and test for multiple channels --- src/prefect/blocks/notifications.py | 38 +++++++++++++------------ tests/blocks/test_notifications.py | 43 ++++++++++++++++++++++++----- 2 files changed, 56 insertions(+), 25 deletions(-) diff --git a/src/prefect/blocks/notifications.py b/src/prefect/blocks/notifications.py index 2bfbbbdf794a..1900fd7be741 100644 --- a/src/prefect/blocks/notifications.py +++ b/src/prefect/blocks/notifications.py @@ -403,36 +403,38 @@ def block_initialization(self) -> None: self._start_apprise_client(url) -class MatterMostWebhook(AbstractAppriseNotificationBlock): +class MattermostWebhook(AbstractAppriseNotificationBlock): """ - Enables sending notifications via a provided MatterMost webhook. - See [Apprise notify_mattermost docs](https://github.com/caronc/apprise/wiki/Notify_mattermost) # noqa + Enables sending notifications via a provided Mattermost webhook. + See [Apprise notify_Mattermost docs](https://github.com/caronc/apprise/wiki/Notify_Mattermost) # noqa Examples: - Load a saved MatterMost webhook and send a message: + Load a saved Mattermost webhook and send a message: ```python - from prefect.blocks.notifications import MatterMostWebhook - mattermost_webhook_block = MatterMostWebhook.load("BLOCK_NAME") - mattermost_webhook_block.notify("Hello from Prefect!") + from prefect.blocks.notifications import MattermostWebhook + + Mattermost_webhook_block = MattermostWebhook.load("BLOCK_NAME") + + Mattermost_webhook_block.notify("Hello from Prefect!") ``` """ - _description = "Enables sending notifications via a provided MatterMost webhook." - _block_type_name = "MatterMost Webhook" - _block_type_slug = "mattermost-webhook" - _logo_url = "https://images.ctfassets.net/zscdif0zqppk/3mlbsJDAmK402ER1sf0zUF/a48ac43fa38f395dd5f56c6ed29f22bb/mattermost-logo-png-transparent.png?h=250" - _documentation_url = "https://docs.prefect.io/api-ref/prefect/blocks/notifications/#prefect.blocks.notifications.MatterMostWebhook" + _description = "Enables sending notifications via a provided Mattermost webhook." + _block_type_name = "Mattermost Webhook" + _block_type_slug = "Mattermost-webhook" + _logo_url = "https://images.ctfassets.net/zscdif0zqppk/3mlbsJDAmK402ER1sf0zUF/a48ac43fa38f395dd5f56c6ed29f22bb/Mattermost-logo-png-transparent.png?h=250" + _documentation_url = "https://docs.prefect.io/api-ref/prefect/blocks/notifications/#prefect.blocks.notifications.MattermostWebhook" hostname: str = Field( default=..., - description="The hostname of your MatterMost server.", - example="mattermost.example.com", + description="The hostname of your Mattermost server.", + example="Mattermost.example.com", ) token: SecretStr = Field( default=..., - description="The token associated with your MatterMost webhook.", + description="The token associated with your Mattermost webhook.", ) botname: Optional[str] = Field( @@ -446,18 +448,18 @@ class MatterMostWebhook(AbstractAppriseNotificationBlock): ) include_image: bool = Field( - default=True, + default=False, description="Whether to include the Apprise status image in the message.", ) fullpath: Optional[str] = Field( default=None, - description="The path of your MatterMost server.", + description="The path of your Mattermost server.", ) port: int = Field( default=8065, - description="The port of your MatterMost server.", + description="The port of your Mattermost server.", ) def block_initialization(self) -> None: diff --git a/tests/blocks/test_notifications.py b/tests/blocks/test_notifications.py index 82433f094bb6..7030dabd2f51 100644 --- a/tests/blocks/test_notifications.py +++ b/tests/blocks/test_notifications.py @@ -8,7 +8,7 @@ import prefect from prefect.blocks.notifications import ( AppriseNotificationBlock, - MatterMostWebhook, + MattermostWebhook, OpsgenieWebhook, PagerDutyWebHook, PrefectNotifyType, @@ -90,7 +90,7 @@ def test_is_picklable(self, block_class: Type[AppriseNotificationBlock]): assert isinstance(unpickled, block_class) -class TestMatterMostWebhook: +class TestMattermostWebhook: async def test_notify_async(self): with patch("apprise.Apprise", autospec=True) as AppriseMock: reload_modules() @@ -98,7 +98,11 @@ async def test_notify_async(self): apprise_instance_mock = AppriseMock.return_value apprise_instance_mock.async_notify = AsyncMock() - mm_block = MatterMostWebhook(hostname="example.com", token="token") + mm_block = MattermostWebhook( + hostname="example.com", + token="token", + include_image=True, + ) await mm_block.notify("test") AppriseMock.assert_called_once() @@ -117,24 +121,49 @@ def test_notify_sync(self): apprise_instance_mock = AppriseMock.return_value apprise_instance_mock.async_notify = AsyncMock() - mm_block = MatterMostWebhook(hostname="example.com", token="token") + mm_block = MattermostWebhook(hostname="example.com", token="token") mm_block.notify("test") AppriseMock.assert_called_once() apprise_instance_mock.add.assert_called_once_with( f"mmost://{mm_block.hostname}/{mm_block.token.get_secret_value()}/" - "?image=yes&format=text&overflow=upstream&rto=4.0&cto=4.0&verify=yes" + "?image=no&format=text&overflow=upstream&rto=4.0&cto=4.0&verify=yes" + ) + apprise_instance_mock.async_notify.assert_called_once_with( + body="test", title=None, notify_type=PrefectNotifyType.DEFAULT + ) + + def test_notify_with_multiple_channels(self): + with patch("apprise.Apprise", autospec=True) as AppriseMock: + reload_modules() + + apprise_instance_mock = AppriseMock.return_value + apprise_instance_mock.async_notify = AsyncMock() + + mm_block = MattermostWebhook( + hostname="example.com", + token="token", + channels=["general", "death-metal-anonymous"], + ) + mm_block.notify("test") + + AppriseMock.assert_called_once() + apprise_instance_mock.add.assert_called_once_with( + f"mmost://{mm_block.hostname}/{mm_block.token.get_secret_value()}/" + "?image=no&format=text&overflow=upstream&rto=4.0&cto=4.0&verify=yes" + "&channel=death-metal-anonymous%2Cgeneral" ) + apprise_instance_mock.async_notify.assert_called_once_with( body="test", title=None, notify_type=PrefectNotifyType.DEFAULT ) def test_is_picklable(self): reload_modules() - block = MatterMostWebhook(token="token", hostname="example.com") + block = MattermostWebhook(token="token", hostname="example.com") pickled = cloudpickle.dumps(block) unpickled = cloudpickle.loads(pickled) - assert isinstance(unpickled, MatterMostWebhook) + assert isinstance(unpickled, MattermostWebhook) class TestOpsgenieWebhook: From 99c52da818ea31a8763368d441c089e7b8fb6424 Mon Sep 17 00:00:00 2001 From: Nathan Nowack Date: Wed, 1 Feb 2023 02:47:59 -0600 Subject: [PATCH 3/4] fix contentful link --- src/prefect/blocks/notifications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/prefect/blocks/notifications.py b/src/prefect/blocks/notifications.py index 1900fd7be741..2eb604714397 100644 --- a/src/prefect/blocks/notifications.py +++ b/src/prefect/blocks/notifications.py @@ -423,7 +423,7 @@ class MattermostWebhook(AbstractAppriseNotificationBlock): _description = "Enables sending notifications via a provided Mattermost webhook." _block_type_name = "Mattermost Webhook" _block_type_slug = "Mattermost-webhook" - _logo_url = "https://images.ctfassets.net/zscdif0zqppk/3mlbsJDAmK402ER1sf0zUF/a48ac43fa38f395dd5f56c6ed29f22bb/Mattermost-logo-png-transparent.png?h=250" + _logo_url = "https://images.ctfassets.net/zscdif0zqppk/3mlbsJDAmK402ER1sf0zUF/a48ac43fa38f395dd5f56c6ed29f22bb/mattermost-logo-png-transparent.png?h=250" _documentation_url = "https://docs.prefect.io/api-ref/prefect/blocks/notifications/#prefect.blocks.notifications.MattermostWebhook" hostname: str = Field( From 63643fd47e0858d90e48a187876a2c738d89f77e Mon Sep 17 00:00:00 2001 From: Nathan Nowack Date: Wed, 1 Feb 2023 10:28:28 -0600 Subject: [PATCH 4/4] address review comments --- src/prefect/blocks/notifications.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/prefect/blocks/notifications.py b/src/prefect/blocks/notifications.py index 2eb604714397..3866a1f7c2b0 100644 --- a/src/prefect/blocks/notifications.py +++ b/src/prefect/blocks/notifications.py @@ -414,15 +414,15 @@ class MattermostWebhook(AbstractAppriseNotificationBlock): ```python from prefect.blocks.notifications import MattermostWebhook - Mattermost_webhook_block = MattermostWebhook.load("BLOCK_NAME") + mattermost_webhook_block = MattermostWebhook.load("BLOCK_NAME") - Mattermost_webhook_block.notify("Hello from Prefect!") + mattermost_webhook_block.notify("Hello from Prefect!") ``` """ _description = "Enables sending notifications via a provided Mattermost webhook." _block_type_name = "Mattermost Webhook" - _block_type_slug = "Mattermost-webhook" + _block_type_slug = "mattermost-webhook" _logo_url = "https://images.ctfassets.net/zscdif0zqppk/3mlbsJDAmK402ER1sf0zUF/a48ac43fa38f395dd5f56c6ed29f22bb/mattermost-logo-png-transparent.png?h=250" _documentation_url = "https://docs.prefect.io/api-ref/prefect/blocks/notifications/#prefect.blocks.notifications.MattermostWebhook" @@ -438,6 +438,7 @@ class MattermostWebhook(AbstractAppriseNotificationBlock): ) botname: Optional[str] = Field( + title="Bot name", default=None, description="The name of the bot that will send the message.", ) @@ -452,9 +453,9 @@ class MattermostWebhook(AbstractAppriseNotificationBlock): description="Whether to include the Apprise status image in the message.", ) - fullpath: Optional[str] = Field( + path: Optional[str] = Field( default=None, - description="The path of your Mattermost server.", + description="An optional sub-path specification to append to the hostname.", ) port: int = Field( @@ -466,7 +467,7 @@ def block_initialization(self) -> None: url = SecretStr( NotifyMattermost( token=self.token.get_secret_value(), - fullpath=self.fullpath, + fullpath=self.path, host=self.hostname, botname=self.botname, channels=self.channels,