diff --git a/README.md b/README.md index 660ce97..a2be1b9 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ This code has only been tested with Python >= 3.6. The library is designed to be used in a seamless way, with minimal code modification: you only need to add a decorator on top your main function call. The return value (if there is one) is also reported in the notification. -There are currently *twelve* ways to setup notifications: +There are currently *thirteen* ways to setup notifications: | Platform | External Contributors | | :-----------------------------------: | :---------------------------------------------------------------------------------------: | @@ -36,6 +36,7 @@ There are currently *twelve* ways to setup notifications: | [DingTalk](#dingtalk) | [@wuutiing](https://github.com/wuutiing) | | [RocketChat](#rocketchat) | [@radao](https://github.com/radao) | | [WeChat Work](#wechat-work) | [@jcyk](https://github.com/jcyk) | +| [Zoho Cliq](#zoho-cliq) | [@raghul-selvaraj-sj](https://github.com/raghul-selvaraj-sj) | ### Email @@ -398,6 +399,33 @@ knockknock wechat \ You can also specify an optional argument to tag specific people: `user-mentions=[""]` and/or `user-mentions-mobile=[""]`. +### Zoho Cliq + +Thanks to [@raghul-selvaraj-sj](https://github.com/raghul-selvaraj-sj), you can also use Zoho Cliq to get notifications. You'll have to get your Cliq Channel/Bot [webhook URL](https://www.zoho.com/cliq/help/platform/webhook-tokens.html). + +#### Python + +```python +from knockknock import cliq_sender + +@cliq_sender(webhook_url="") +def train_your_nicest_model(your_nicest_parameters): + import time + time.sleep(10) + return {'loss': 0.9} # Optional return value +``` + +#### Command-line + +```bash +knockknock cliq \ + --webhook-url \ + sleep 10 +``` + +You can also specify an optional argument to tag specific people: `user_mentions=[, ]`. + + ## Note on distributed training When using distributed training, a GPU is bound to its process using the local rank variable. Since knockknock works at the process level, if you are using 8 GPUs, you would get 8 notifications at the beginning and 8 notifications at the end... To circumvent that, except for errors, only the master process is allowed to send notifications so that you receive only one notification at the beginning and one notification at the end. diff --git a/knockknock/__init__.py b/knockknock/__init__.py index a5c7f51..c6fe8ee 100644 --- a/knockknock/__init__.py +++ b/knockknock/__init__.py @@ -10,3 +10,4 @@ from knockknock.dingtalk_sender import dingtalk_sender from knockknock.wechat_sender import wechat_sender from knockknock.rocketchat_sender import rocketchat_sender +from knockknock.cliq_sender import cliq_sender diff --git a/knockknock/__main__.py b/knockknock/__main__.py index 215939d..6ca3863 100644 --- a/knockknock/__main__.py +++ b/knockknock/__main__.py @@ -12,7 +12,8 @@ sms_sender, teams_sender, telegram_sender, - wechat_sender,) + wechat_sender, + cliq_sender, ) def main(): parser = argparse.ArgumentParser( @@ -33,6 +34,18 @@ def main(): help="Optional user alias or full email address to notify, as comma separated list.") chime_parser.set_defaults(sender_func=chime_sender) + # Zoho Cliq + cliq_parser = subparsers.add_parser( + name="cliq", description="Send a Cliq message before and after function " + + "execution, with start and end status (successfully or crashed).") + cliq_parser.add_argument( + "--webhook-url", type=str, required=True, + help="The webhook URL to access your Cliq channel/bot.") + cliq_parser.add_argument( + "--user-mentions", type=lambda s: s.split(","), required=False, default=[], + help="Optional user ids to notify, as comma seperated list.") + cliq_parser.set_defaults(sender_func=cliq_sender) + # Desktop desktop_parser = subparsers.add_parser( name="desktop", description="Send a desktop notification before and after function " + diff --git a/knockknock/cliq_sender.py b/knockknock/cliq_sender.py new file mode 100644 index 0000000..5c9c47c --- /dev/null +++ b/knockknock/cliq_sender.py @@ -0,0 +1,109 @@ +import datetime +import functools +import json +import os +import socket +import traceback +from typing import List + +import requests + +DATE_FORMAT = "%Y-%m-%d %H:%M:%S" + + +def cliq_sender(webhook_url: str, user_mentions: List[str] = []): + """ + cliq sender wrapper: execute func, send a Cliq message with the end status + (successfully finished or crashed) at the end. Also send a Cliq message before + executing func. + + `webhook_url`: str + The webhook URL to access your channel/bot. + Visit https://www.zoho.com/cliq/help/platform/webhook-tokens.html for more details. + `user_mentions`: List[str] (default=[]) + Optional users ids to notify. + """ + + # Visit https://www.zoho.com/cliq/help/restapi/v2/#cliq_rest_api for more api related details + user_mentions = ['{@' + str(user_mention) + '}' for user_mention in user_mentions] + + dump = { + "bot": { + "name": "Knock Knock", + "image": "https://huggingface.co/front/assets/huggingface_logo.svg", + } + } + + def decorator_sender(func): + @functools.wraps(func) + def wrapper_sender(*args, **kwargs): + start_time = datetime.datetime.now() + host_name = socket.gethostname() + func_name = func.__name__ + + # Handling distributed training edge case. + # In PyTorch, the launch of `torch.distributed.launch` sets up a RANK environment variable for each process. + # This can be used to detect the master process. + # See https://github.com/pytorch/pytorch/blob/master/torch/distributed/launch.py#L211 + # Except for errors, only the master process will send notifications. + if 'RANK' in os.environ: + master_process = (int(os.environ['RANK']) == 0) + host_name += ' - RANK: %s' % os.environ['RANK'] + else: + master_process = True + + if master_process: + contents = ['Your training has started 🎬', + 'Machine name: %s' % host_name, + 'Main call: %s' % func_name, + 'Starting date: %s' % start_time.strftime(DATE_FORMAT)] + contents.append(' '.join(user_mentions)) + dump['text'] = '\n'.join(contents) + requests.post(webhook_url, json.dumps(dump)) + + try: + value = func(*args, **kwargs) + + if master_process: + end_time = datetime.datetime.now() + elapsed_time = end_time - start_time + contents = ["Your training is complete 🎉", + 'Machine name: %s' % host_name, + 'Main call: %s' % func_name, + 'Starting date: %s' % start_time.strftime(DATE_FORMAT), + 'End date: %s' % end_time.strftime(DATE_FORMAT), + 'Training duration: %s' % str(elapsed_time)] + + try: + str_value = str(value) + contents.append('Main call returned value: %s' % str_value) + except: + contents.append('Main call returned value: %s' % "ERROR - Couldn't str the returned value.") + + contents.append(' '.join(user_mentions)) + dump['text'] = '\n'.join(contents) + requests.post(webhook_url, json.dumps(dump)) + + return value + + except Exception as ex: + end_time = datetime.datetime.now() + elapsed_time = end_time - start_time + contents = ["Your training has crashed ☠️", + 'Machine name: %s' % host_name, + 'Main call: %s' % func_name, + 'Starting date: %s' % start_time.strftime(DATE_FORMAT), + 'Crash date: %s' % end_time.strftime(DATE_FORMAT), + 'Crashed training duration: %s\n\n' % str(elapsed_time), + "Here's the error:", + '%s\n\n' % ex, + "Traceback:", + '%s' % traceback.format_exc()] + contents.append(' '.join(user_mentions)) + dump['text'] = '\n'.join(contents) + requests.post(webhook_url, json.dumps(dump)) + raise ex + + return wrapper_sender + + return decorator_sender