Skip to content

Commit

Permalink
Add mattermost contrib package.
Browse files Browse the repository at this point in the history
  • Loading branch information
riga committed Jan 3, 2025
1 parent 5c6c285 commit 511d6ed
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 1 deletion.
1 change: 1 addition & 0 deletions docs/contrib/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ The following example shows how a package (e.g. the :py:mod:`~law.docker` packag
keras
lsf
matplotlib
mattermost
mercurial
numpy
pandas
Expand Down
19 changes: 19 additions & 0 deletions docs/contrib/mattermost.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
mattermost
==========

.. automodule:: law.mattermost

.. contents::


Function ``notify_mattermost``
------------------------------

.. autofunction:: notify_mattermost


Class ``NotifyMattermostParameter``
-----------------------------------

.. autoclass:: NotifyMattermostParameter
:members:
49 changes: 49 additions & 0 deletions law.cfg.example
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,55 @@
; Type: string
; Default: None

; mattermost_hook_url
; Desciption: Address of the mattermost webhook to use. For more info, see
; https://developers.mattermost.com/integrate/webhooks/incoming.
; Type: string
; Default: None

; mattermost_header
; Desciption: A custom header to add to mattermost notifications. Not used when empty.
; Type: string
; Default: None

; mattermost_channel
; Desciption: A custom channel name to send the notification to. When empty, the default configured
; for the webhook is used.
; Type: string
; Default: None

; mattermost_user
; Desciption: A custom user name to send the notification with. When empty, the default configured
; for the webhook is used.
; Type: string
; Default: None

; mattermost_mention_user
; Desciption: If set, the user with that name is mentioned.
; Type: string
; Default: None

; mattermost_icon_url
; Desciption: The address of a custom icon to use. When empty, the default configured for the
; webhook is used.
; Type: string
; Default: "https://media.githubusercontent.com/media/riga/law/refs/heads/master/assets/logo_profile.png"

; mattermost_icon_emoji
; Desciption: Name of a custom emoji to use instead of the icon url.
; Type: string
; Default: None

; mattermost_success_emoji
; Desciption: Emoji to append to the status text in case of success.
; Type: string
; Default: ":tada:"

; mattermost_failure_emoji
; Desciption: Emoji to append to the status text in case of a failure.
; Type: string
; Default: ":rotating_light:"


; --- bash_sandbox section -------------------------------------------------------------------------

Expand Down
2 changes: 1 addition & 1 deletion law/cli/completion.sh
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ _law_complete() {
# complete the "location" subcommand
elif [ "${sub_cmd}" = "location" ]; then
local words="help"
local contribs="arc awkward cms coffea docker dropbox gfal git glite hdf5 htcondor ipython keras lsf matplotlib mercurial numpy pandas profiling pyarrow rich root singularity slack slurm tasks telegram tensorflow wlcg"
local contribs="arc awkward cms coffea docker dropbox gfal git glite hdf5 htcondor ipython keras lsf matplotlib mattermost mercurial numpy pandas profiling pyarrow rich root singularity slack slurm tasks telegram tensorflow wlcg"
local inp="${cur##-}"
inp="${inp##-}"
COMPREPLY=( $( compgen -W "$( echo ${words} )" -P "--" -- "${inp}" ) $( compgen -W "$( echo ${contribs} )" -- "${inp}" ) )
Expand Down
13 changes: 13 additions & 0 deletions law/contrib/mattermost/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# coding: utf-8
# flake8: noqa

"""
Mattermost contrib functionality.
"""

__all__ = ["notify_mattermost", "NotifyMattermostParameter"]


# provisioning imports
from law.contrib.mattermost.notification import notify_mattermost
from law.contrib.mattermost.parameter import NotifyMattermostParameter
21 changes: 21 additions & 0 deletions law/contrib/mattermost/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# coding: utf-8

"""
Function returning the config defaults of the mattermost package.
"""


def config_defaults(default_config):
return {
"notifications": {
"mattermost_hook_url": None,
"mattermost_header": None,
"mattermost_channel": None,
"mattermost_user": None,
"mattermost_mention_user": None,
"mattermost_icon_url": "https://media.githubusercontent.com/media/riga/law/refs/heads/master/assets/logo_profile.png", # noqa
"mattermost_icon_emoji": None,
"mattermost_success_emoji": ":tada:",
"mattermost_failure_emoji": ":rotating_light:",
},
}
102 changes: 102 additions & 0 deletions law/contrib/mattermost/notification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# coding: utf-8

"""
Mattermost notifications.
"""

__all__ = ["notify_mattermost"]

import threading

from law.config import Config
from law.util import escape_markdown
from law.logger import get_logger


logger = get_logger(__name__)


def notify_mattermost(
title,
content,
hook_url=None,
channel=None,
user=None,
mention_user=None,
icon_url=None,
icon_emoji=None,
**kwargs,
):
"""
Sends a slack notification and returns *True* on success. The communication with the slack API
might have some delays and is therefore handled by a thread. The format of the notification
depends on *content*. If it is a string, a simple text notification is sent. Otherwise, it
should be a dictionary whose fields are used to build a message attachment with two-column
formatting.
"""
cfg = Config.instance()

# get default settings
if not hook_url:
hook_url = cfg.get_expanded("notifications", "mattermost_hook_url")
if not channel:
channel = cfg.get_expanded("notifications", "mattermost_channel")
if not user:
user = cfg.get_expanded("notifications", "mattermost_user")
if not mention_user:
mention_user = cfg.get_expanded("notifications", "mattermost_mention_user")
if not icon_url:
icon_url = cfg.get_expanded("notifications", "mattermost_icon_url")
if not icon_emoji:
icon_emoji = cfg.get_expanded("notifications", "mattermost_icon_emoji")

if not hook_url:
logger.warning("cannot send Mattermost notification, hook_url ({}) empty".format(hook_url))
return False

# append the user to mention to the title
# unless explicitly set to empty string
mention_text = ""
if mention_user:
mention_text = " (@{})".format(escape_markdown(mention_user.lstrip("@")))

# request data for the API call
request_data = {}
if channel:
request_data["channel"] = channel
if user:
request_data["username"] = user
if icon_url:
request_data["icon_url"] = icon_url
if icon_emoji:
request_data["icon_emoji"] = icon_emoji

# standard or attachment content?
request_data["text"] = "{}{}\n\n".format(title, mention_text)
if isinstance(content, str):
request_data["text"] += content
else:
for k, v in content.items():
request_data["text"] += "{}: {}\n".format(k, v)

# extend by arbitrary kwargs
request_data.update(kwargs)

# threaded, non-blocking API communication
thread = threading.Thread(target=_notify_mattermost, args=(hook_url, request_data))
thread.start()

return True


def _notify_mattermost(hook_url, request_data):
import traceback
import requests

try:
res = requests.post(hook_url, json=request_data)
if not res.ok:
logger.warning("unsuccessful Mattermost API call: {}".format(res))
except Exception as e:
t = traceback.format_exc()
logger.warning("could not send Mattermost notification: {}\n{}".format(e, t))
66 changes: 66 additions & 0 deletions law/contrib/mattermost/parameter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# coding: utf-8

"""
Mattermost-related parameters.
"""

__all__ = ["NotifyMattermostParameter"]

from collections import OrderedDict

from law.config import Config
from law.parameter import NotifyParameter
from law.contrib.mattermost.notification import notify_mattermost


class NotifyMattermostParameter(NotifyParameter):

def __init__(self, *args, **kwargs):
super(NotifyMattermostParameter, self).__init__(*args, **kwargs)

if not self.description:
self.description = (
"when true, and the task's run method is decorated with law.decorator.notify, "
"a Mattermost notification is sent once the task finishes"
)

@staticmethod
def notify(success, title, content, **kwargs):
content = OrderedDict(content)

# overwrite title
cfg = Config.instance()
header = cfg.get_expanded("notifications", "mattermost_header")
task_block = "```\n{}\n```".format(content["Task"])
title = "{}\n{}".format(header, task_block) if header else task_block
del content["Task"]

# markup for traceback
if "Traceback" in content:
content["Traceback"] = "\n```\n{}\n```".format(content["Traceback"])

# prepend the status text to the message content
cfg = Config.instance()
status_text = "success" if success else "failure"
status_emoji = cfg.get_expanded("notifications", "mattermost_{}_emoji".format(status_text))
if status_emoji:
status_text += " " + status_emoji
content["Status"] = status_text
content.move_to_end("Status", last=False)

# highlight last message
if "Last message" in content:
content["Last message"] = "`{}`".format(content["Last message"])

# highlight keys
content = content.__class__(("**{}**".format(k), v) for k, v in content.items())

# send the notification
return notify_mattermost(title, content, **kwargs)

def get_transport(self):
return {
"func": self.notify,
"raw": True,
"colored": False,
}

0 comments on commit 511d6ed

Please sign in to comment.