From bb69e6de46a172bfe6b19759ed75eed0327219c9 Mon Sep 17 00:00:00 2001 From: Helio Machado <0x2b3bfa0+git@googlemail.com> Date: Sat, 2 Apr 2022 23:02:07 +0200 Subject: [PATCH 1/7] add contrib.slack --- .meta/.readme.rst | 4 +- README.rst | 4 +- setup.cfg | 1 + tqdm/contrib/bells.py | 2 + tqdm/contrib/slack.py | 120 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 tqdm/contrib/slack.py diff --git a/.meta/.readme.rst b/.meta/.readme.rst index 65ffb181..771e884c 100644 --- a/.meta/.readme.rst +++ b/.meta/.readme.rst @@ -474,10 +474,11 @@ The ``tqdm.contrib`` package also contains experimental modules: - ``tqdm.contrib.itertools``: Thin wrappers around ``itertools`` - ``tqdm.contrib.concurrent``: Thin wrappers around ``concurrent.futures`` - ``tqdm.contrib.discord``: Posts to `Discord `__ bots +- ``tqdm.contrib.slack``: Posts to `Slack `__ bots - ``tqdm.contrib.telegram``: Posts to `Telegram `__ bots - ``tqdm.contrib.bells``: Automagically enables all optional features - * ``auto``, ``pandas``, ``discord``, ``telegram`` + * ``auto``, ``pandas``, ``discord``, ``slack``, ``telegram`` Examples and Advanced Usage --------------------------- @@ -967,6 +968,7 @@ Some submodule examples of inheritance: - `tqdm/tk.py `__ - `tqdm/contrib/telegram.py `__ - `tqdm/contrib/discord.py `__ +- `tqdm/contrib/slack.py `__ Dynamic Monitor/Meter ~~~~~~~~~~~~~~~~~~~~~ diff --git a/README.rst b/README.rst index 34983795..ff4969b6 100644 --- a/README.rst +++ b/README.rst @@ -693,10 +693,11 @@ The ``tqdm.contrib`` package also contains experimental modules: - ``tqdm.contrib.itertools``: Thin wrappers around ``itertools`` - ``tqdm.contrib.concurrent``: Thin wrappers around ``concurrent.futures`` - ``tqdm.contrib.discord``: Posts to `Discord `__ bots +- ``tqdm.contrib.slack``: Posts to `Slack `__ bots - ``tqdm.contrib.telegram``: Posts to `Telegram `__ bots - ``tqdm.contrib.bells``: Automagically enables all optional features - * ``auto``, ``pandas``, ``discord``, ``telegram`` + * ``auto``, ``pandas``, ``discord``, ``slack``, ``telegram`` Examples and Advanced Usage --------------------------- @@ -1185,6 +1186,7 @@ Some submodule examples of inheritance: - `tqdm/gui.py `__ - `tqdm/tk.py `__ - `tqdm/contrib/telegram.py `__ +- `tqdm/contrib/slack.py `__ - `tqdm/contrib/discord.py `__ Dynamic Monitor/Meter diff --git a/setup.cfg b/setup.cfg index e43ab21a..06b1cedd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -138,6 +138,7 @@ include=tqdm/* omit= tqdm/contrib/bells.py tqdm/contrib/discord.py + tqdm/contrib/slack.py tqdm/contrib/telegram.py tqdm/contrib/utils_worker.py relative_files=True diff --git a/tqdm/contrib/bells.py b/tqdm/contrib/bells.py index be227688..a7abbe7f 100644 --- a/tqdm/contrib/bells.py +++ b/tqdm/contrib/bells.py @@ -14,6 +14,8 @@ from os import getenv if getenv("TQDM_TELEGRAM_TOKEN") and getenv("TQDM_TELEGRAM_CHAT_ID"): from .telegram import tqdm, trange +elif getenv("TQDM_SLACK_TOKEN") and getenv("TQDM_SLACK_CHANNEL"): + from .slack import tqdm, trange elif getenv("TQDM_DISCORD_TOKEN") and getenv("TQDM_DISCORD_CHANNEL_ID"): from .discord import tqdm, trange else: diff --git a/tqdm/contrib/slack.py b/tqdm/contrib/slack.py new file mode 100644 index 00000000..bbe6d28f --- /dev/null +++ b/tqdm/contrib/slack.py @@ -0,0 +1,120 @@ +""" +Sends updates to a Slack app. +Usage: +>>> from tqdm.contrib.slack import tqdm, trange +>>> for i in tqdm(iterable, token='{token}', channel='{channel}'): +... ... +![screenshot](https://img.tqdm.ml/screenshot-slack.png) +""" +from __future__ import absolute_import + +import logging +from os import getenv + +try: + from slack_sdk import WebClient +except ImportError: + raise ImportError("Please `pip install slack-sdk`") + +from ..auto import tqdm as tqdm_auto +from ..utils import _range +from .utils_worker import MonoWorker + +__author__ = {"github.com/": ["0x2b3bfa0"]} +__all__ = ['SlackIO', 'tqdm_slack', 'tsrange', 'tqdm', 'trange'] + + +class SlackIO(MonoWorker): + """Non-blocking file-like IO using a Slack app.""" + def __init__(self, token, channel): + """Creates a new message in the given `channel`.""" + super(SlackIO, self).__init__() + self.client = WebClient(token=token) + self.channel = channel + self.text = self.__class__.__name__ + try: + self.message = self.client.chat_postMessage( + channel=channel, text=self.text) + except Exception as e: + tqdm_auto.write(str(e)) + + def write(self, s): + """Replaces internal `message`'s text with `s`.""" + if not s: + s = "..." + s = s.replace('\r', '').strip() + if s == self.text: + return # skip duplicate message + self.text = s + try: + future = self.submit( + self.client.chat_update, + channel=self.message["channel"], + ts=self.message["ts"], + text='`' + s + '`') + except Exception as e: + tqdm_auto.write(str(e)) + else: + return future + + +class tqdm_slack(tqdm_auto): + """ + Standard `tqdm.auto.tqdm` but also sends updates to a Slack app. + May take a few seconds to create (`__init__`). + + - create a Slack app with the `chat:write` scope & invite it to a + channel: + - copy the bot `{token}` & `{channel}` and paste below + >>> from tqdm.contrib.slack import tqdm, trange + >>> for i in tqdm(iterable, token='{token}', channel='{channel}'): + ... ... + """ + def __init__(self, *args, **kwargs): + """ + Parameters + ---------- + token : str, required. Slack token + [default: ${TQDM_SLACK_TOKEN}]. + channel : int, required. Slack channel + [default: ${TQDM_SLACK_CHANNEL}]. + mininterval : float, optional. + Minimum of [default: 1.5] to avoid rate limit. + See `tqdm.auto.tqdm.__init__` for other parameters. + """ + if not kwargs.get('disable'): + kwargs = kwargs.copy() + logging.getLogger("HTTPClient").setLevel(logging.WARNING) + self.sio = SlackIO( + kwargs.pop('token', getenv("TQDM_SLACK_TOKEN")), + kwargs.pop('channel', getenv("TQDM_SLACK_CHANNEL"))) + kwargs['mininterval'] = max(1.5, kwargs.get('mininterval', 1.5)) + super(tqdm_slack, self).__init__(*args, **kwargs) + + def display(self, **kwargs): + super(tqdm_slack, self).display(**kwargs) + fmt = self.format_dict + if fmt.get('bar_format', None): + fmt['bar_format'] = fmt['bar_format'].replace( + '', '{bar:10u}').replace('{bar}', '{bar:10u}') + else: + fmt['bar_format'] = '{l_bar}{bar:10u}{r_bar}' + self.sio.write(self.format_meter(**fmt)) + + def clear(self, *args, **kwargs): + super(tqdm_slack, self).clear(*args, **kwargs) + if not self.disable: + self.sio.write("") + + +def tsrange(*args, **kwargs): + """ + A shortcut for `tqdm.contrib.slack.tqdm(xrange(*args), **kwargs)`. + On Python3+, `range` is used instead of `xrange`. + """ + return tqdm_slack(_range(*args), **kwargs) + + +# Aliases +tqdm = tqdm_slack +trange = tsrange From bdf32a7c70ebda1edf2ea9eb53aac416f594658d Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Mon, 4 Apr 2022 00:50:33 +0100 Subject: [PATCH 2/7] slight tidy --- tqdm/contrib/discord.py | 2 +- tqdm/contrib/slack.py | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/tqdm/contrib/discord.py b/tqdm/contrib/discord.py index 713a2f82..8eae8e2d 100644 --- a/tqdm/contrib/discord.py +++ b/tqdm/contrib/discord.py @@ -3,7 +3,7 @@ Sends updates to a Discord bot. Usage: >>> from tqdm.contrib.discord import tqdm, trange ->>> for i in tqdm(iterable, token='{token}', channel_id='{channel_id}'): +>>> for i in trange(10, token='{token}', channel_id='{channel_id}'): ... ... ![screenshot](https://img.tqdm.ml/screenshot-discord.png) diff --git a/tqdm/contrib/slack.py b/tqdm/contrib/slack.py index bbe6d28f..e55a16ca 100644 --- a/tqdm/contrib/slack.py +++ b/tqdm/contrib/slack.py @@ -1,9 +1,11 @@ """ Sends updates to a Slack app. + Usage: >>> from tqdm.contrib.slack import tqdm, trange ->>> for i in tqdm(iterable, token='{token}', channel='{channel}'): +>>> for i in trange(10, token='{token}', channel='{channel}'): ... ... + ![screenshot](https://img.tqdm.ml/screenshot-slack.png) """ from __future__ import absolute_import @@ -33,8 +35,7 @@ class SlackIO(MonoWorker): self.channel = channel self.text = self.__class__.__name__ try: - self.message = self.client.chat_postMessage( - channel=channel, text=self.text) + self.message = self.client.chat_postMessage(channel=channel, text=self.text) except Exception as e: tqdm_auto.write(str(e)) @@ -47,11 +48,8 @@ class SlackIO(MonoWorker): return # skip duplicate message self.text = s try: - future = self.submit( - self.client.chat_update, - channel=self.message["channel"], - ts=self.message["ts"], - text='`' + s + '`') + future = self.submit(self.client.chat_update, channel=self.message["channel"], + ts=self.message["ts"], text='`' + s + '`') except Exception as e: tqdm_auto.write(str(e)) else: @@ -80,6 +78,7 @@ class tqdm_slack(tqdm_auto): [default: ${TQDM_SLACK_CHANNEL}]. mininterval : float, optional. Minimum of [default: 1.5] to avoid rate limit. + See `tqdm.auto.tqdm.__init__` for other parameters. """ if not kwargs.get('disable'): From a1d4401f186dc5a79b4ad452f38cae75e1f2e6da Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Mon, 4 Apr 2022 00:50:58 +0100 Subject: [PATCH 3/7] remove unneeded variable --- tqdm/contrib/slack.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tqdm/contrib/slack.py b/tqdm/contrib/slack.py index e55a16ca..a3b179a9 100644 --- a/tqdm/contrib/slack.py +++ b/tqdm/contrib/slack.py @@ -32,7 +32,6 @@ class SlackIO(MonoWorker): """Creates a new message in the given `channel`.""" super(SlackIO, self).__init__() self.client = WebClient(token=token) - self.channel = channel self.text = self.__class__.__name__ try: self.message = self.client.chat_postMessage(channel=channel, text=self.text) From 7994aa8285743b351cf1a3b36275335d8d0730b7 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Mon, 4 Apr 2022 00:51:27 +0100 Subject: [PATCH 4/7] warn once on error --- tqdm/contrib/discord.py | 6 +++++- tqdm/contrib/slack.py | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/tqdm/contrib/discord.py b/tqdm/contrib/discord.py index 8eae8e2d..0edd35cc 100644 --- a/tqdm/contrib/discord.py +++ b/tqdm/contrib/discord.py @@ -39,6 +39,7 @@ class DiscordIO(MonoWorker): self.message = client.api.channels_messages_create(channel_id, self.text) except Exception as e: tqdm_auto.write(str(e)) + self.message = None def write(self, s): """Replaces internal `message`'s text with `s`.""" @@ -47,9 +48,12 @@ class DiscordIO(MonoWorker): s = s.replace('\r', '').strip() if s == self.text: return # skip duplicate message + message = self.message + if message is None: + return self.text = s try: - future = self.submit(self.message.edit, '`' + s + '`') + future = self.submit(message.edit, '`' + s + '`') except Exception as e: tqdm_auto.write(str(e)) else: diff --git a/tqdm/contrib/slack.py b/tqdm/contrib/slack.py index a3b179a9..0a26c6e9 100644 --- a/tqdm/contrib/slack.py +++ b/tqdm/contrib/slack.py @@ -37,6 +37,7 @@ class SlackIO(MonoWorker): self.message = self.client.chat_postMessage(channel=channel, text=self.text) except Exception as e: tqdm_auto.write(str(e)) + self.message = None def write(self, s): """Replaces internal `message`'s text with `s`.""" @@ -45,10 +46,13 @@ class SlackIO(MonoWorker): s = s.replace('\r', '').strip() if s == self.text: return # skip duplicate message + message = self.message + if message is None: + return self.text = s try: - future = self.submit(self.client.chat_update, channel=self.message["channel"], - ts=self.message["ts"], text='`' + s + '`') + future = self.submit(self.client.chat_update, channel=message["channel"], + ts=message["ts"], text='`' + s + '`') except Exception as e: tqdm_auto.write(str(e)) else: From bf6c960f60f8a390b47ac55d2ece3ffc419e5dcd Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Mon, 4 Apr 2022 00:52:29 +0100 Subject: [PATCH 5/7] emoji bars --- environment.yml | 2 +- tqdm/contrib/slack.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/environment.yml b/environment.yml index d4580dc3..37c2fbc0 100644 --- a/environment.yml +++ b/environment.yml @@ -36,7 +36,7 @@ dependencies: - wheel # `setup.py bdist_wheel` # `cd docs && pymake` - mkdocs-material -- pydoc-markdown >=4.6.0 +- pydoc-markdown - pygments - pymdown-extensions - pip: diff --git a/tqdm/contrib/slack.py b/tqdm/contrib/slack.py index 0a26c6e9..336ac2ec 100644 --- a/tqdm/contrib/slack.py +++ b/tqdm/contrib/slack.py @@ -98,9 +98,13 @@ class tqdm_slack(tqdm_auto): fmt = self.format_dict if fmt.get('bar_format', None): fmt['bar_format'] = fmt['bar_format'].replace( - '', '{bar:10u}').replace('{bar}', '{bar:10u}') + '', '`{bar:10}`').replace('{bar}', '`{bar:10u}`') else: - fmt['bar_format'] = '{l_bar}{bar:10u}{r_bar}' + fmt['bar_format'] = '{l_bar}`{bar:10}`{r_bar}' + if fmt['ascii'] is False: + fmt['ascii'] = [":black_square:", ":small_blue_diamond:", ":large_blue_diamond:", + ":large_blue_square:"] + fmt['ncols'] = 336 self.sio.write(self.format_meter(**fmt)) def clear(self, *args, **kwargs): From 4a1d10e19fdca00db47fd50725715dc5e4aa68e6 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Mon, 4 Apr 2022 01:03:21 +0100 Subject: [PATCH 6/7] consistent ordering --- .meta/.readme.rst | 8 ++++---- README.rst | 6 +++--- setup.cfg | 2 +- tqdm/contrib/bells.py | 6 +++--- tqdm/contrib/slack.py | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.meta/.readme.rst b/.meta/.readme.rst index 771e884c..23aac077 100644 --- a/.meta/.readme.rst +++ b/.meta/.readme.rst @@ -473,12 +473,12 @@ The ``tqdm.contrib`` package also contains experimental modules: - ``tqdm.contrib.itertools``: Thin wrappers around ``itertools`` - ``tqdm.contrib.concurrent``: Thin wrappers around ``concurrent.futures`` -- ``tqdm.contrib.discord``: Posts to `Discord `__ bots - ``tqdm.contrib.slack``: Posts to `Slack `__ bots +- ``tqdm.contrib.discord``: Posts to `Discord `__ bots - ``tqdm.contrib.telegram``: Posts to `Telegram `__ bots - ``tqdm.contrib.bells``: Automagically enables all optional features - * ``auto``, ``pandas``, ``discord``, ``slack``, ``telegram`` + * ``auto``, ``pandas``, ``slack``, ``discord``, ``telegram`` Examples and Advanced Usage --------------------------- @@ -966,9 +966,9 @@ Some submodule examples of inheritance: - `tqdm/notebook.py `__ - `tqdm/gui.py `__ - `tqdm/tk.py `__ -- `tqdm/contrib/telegram.py `__ -- `tqdm/contrib/discord.py `__ - `tqdm/contrib/slack.py `__ +- `tqdm/contrib/discord.py `__ +- `tqdm/contrib/telegram.py `__ Dynamic Monitor/Meter ~~~~~~~~~~~~~~~~~~~~~ diff --git a/README.rst b/README.rst index ff4969b6..a45f98d2 100644 --- a/README.rst +++ b/README.rst @@ -692,12 +692,12 @@ The ``tqdm.contrib`` package also contains experimental modules: - ``tqdm.contrib.itertools``: Thin wrappers around ``itertools`` - ``tqdm.contrib.concurrent``: Thin wrappers around ``concurrent.futures`` -- ``tqdm.contrib.discord``: Posts to `Discord `__ bots - ``tqdm.contrib.slack``: Posts to `Slack `__ bots +- ``tqdm.contrib.discord``: Posts to `Discord `__ bots - ``tqdm.contrib.telegram``: Posts to `Telegram `__ bots - ``tqdm.contrib.bells``: Automagically enables all optional features - * ``auto``, ``pandas``, ``discord``, ``slack``, ``telegram`` + * ``auto``, ``pandas``, ``slack``, ``discord``, ``telegram`` Examples and Advanced Usage --------------------------- @@ -1185,9 +1185,9 @@ Some submodule examples of inheritance: - `tqdm/notebook.py `__ - `tqdm/gui.py `__ - `tqdm/tk.py `__ -- `tqdm/contrib/telegram.py `__ - `tqdm/contrib/slack.py `__ - `tqdm/contrib/discord.py `__ +- `tqdm/contrib/telegram.py `__ Dynamic Monitor/Meter ~~~~~~~~~~~~~~~~~~~~~ diff --git a/setup.cfg b/setup.cfg index 06b1cedd..63d32add 100644 --- a/setup.cfg +++ b/setup.cfg @@ -137,8 +137,8 @@ branch=True include=tqdm/* omit= tqdm/contrib/bells.py - tqdm/contrib/discord.py tqdm/contrib/slack.py + tqdm/contrib/discord.py tqdm/contrib/telegram.py tqdm/contrib/utils_worker.py relative_files=True diff --git a/tqdm/contrib/bells.py b/tqdm/contrib/bells.py index a7abbe7f..5b8f4b9e 100644 --- a/tqdm/contrib/bells.py +++ b/tqdm/contrib/bells.py @@ -12,10 +12,10 @@ __all__ = ['tqdm', 'trange'] import warnings from os import getenv -if getenv("TQDM_TELEGRAM_TOKEN") and getenv("TQDM_TELEGRAM_CHAT_ID"): - from .telegram import tqdm, trange -elif getenv("TQDM_SLACK_TOKEN") and getenv("TQDM_SLACK_CHANNEL"): +if getenv("TQDM_SLACK_TOKEN") and getenv("TQDM_SLACK_CHANNEL"): from .slack import tqdm, trange +elif getenv("TQDM_TELEGRAM_TOKEN") and getenv("TQDM_TELEGRAM_CHAT_ID"): + from .telegram import tqdm, trange elif getenv("TQDM_DISCORD_TOKEN") and getenv("TQDM_DISCORD_CHANNEL_ID"): from .discord import tqdm, trange else: diff --git a/tqdm/contrib/slack.py b/tqdm/contrib/slack.py index 336ac2ec..6a1b1081 100644 --- a/tqdm/contrib/slack.py +++ b/tqdm/contrib/slack.py @@ -51,8 +51,8 @@ class SlackIO(MonoWorker): return self.text = s try: - future = self.submit(self.client.chat_update, channel=message["channel"], - ts=message["ts"], text='`' + s + '`') + future = self.submit(self.client.chat_update, channel=message['channel'], + ts=message['ts'], text='`' + s + '`') except Exception as e: tqdm_auto.write(str(e)) else: From 1d29dec4b07de3dab34d3557baa9520cd9d46e38 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Mon, 4 Apr 2022 01:20:25 +0100 Subject: [PATCH 7/7] add `[slack]` extra dependency --- environment.yml | 3 ++- setup.cfg | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index 37c2fbc0..871e3e8e 100644 --- a/environment.yml +++ b/environment.yml @@ -29,6 +29,7 @@ dependencies: - numpy # pandas, keras, contrib.tenumerate - pandas - tensorflow # keras +- slack-sdk # contrib.slack - requests # contrib.telegram - rich # rich - argopt # `cd wiki && pymake` @@ -41,5 +42,5 @@ dependencies: - pymdown-extensions - pip: - py-make >=0.1.0 # `setup.py make/pymake` - - mkdocs-minify-plugin # `cd docs && pymake` + - mkdocs-minify-plugin # `cd docs && pymake` - git+https://github.com/tqdm/jsmin@python3-only#egg=jsmin # `cd docs && pymake` diff --git a/setup.cfg b/setup.cfg index 63d32add..82152122 100644 --- a/setup.cfg +++ b/setup.cfg @@ -84,6 +84,7 @@ include_package_data=True packages=find: [options.extras_require] dev=py-make>=0.1.0; twine; wheel +slack=slack-sdk telegram=requests notebook=ipywidgets>=6 [options.entry_points]