diff --git a/.meta/.readme.rst b/.meta/.readme.rst index 65ffb181..23aac077 100644 --- a/.meta/.readme.rst +++ b/.meta/.readme.rst @@ -473,11 +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.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``, ``telegram`` + * ``auto``, ``pandas``, ``slack``, ``discord``, ``telegram`` Examples and Advanced Usage --------------------------- @@ -965,8 +966,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/README.rst b/README.rst index 34983795..a45f98d2 100644 --- a/README.rst +++ b/README.rst @@ -692,11 +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.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``, ``telegram`` + * ``auto``, ``pandas``, ``slack``, ``discord``, ``telegram`` Examples and Advanced Usage --------------------------- @@ -1184,8 +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/environment.yml b/environment.yml index d4580dc3..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` @@ -36,10 +37,10 @@ dependencies: - wheel # `setup.py bdist_wheel` # `cd docs && pymake` - mkdocs-material -- pydoc-markdown >=4.6.0 +- pydoc-markdown - pygments - 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 e43ab21a..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] @@ -137,6 +138,7 @@ branch=True include=tqdm/* omit= tqdm/contrib/bells.py + tqdm/contrib/slack.py tqdm/contrib/discord.py tqdm/contrib/telegram.py tqdm/contrib/utils_worker.py diff --git a/tqdm/contrib/bells.py b/tqdm/contrib/bells.py index be227688..5b8f4b9e 100644 --- a/tqdm/contrib/bells.py +++ b/tqdm/contrib/bells.py @@ -12,7 +12,9 @@ __all__ = ['tqdm', 'trange'] import warnings from os import getenv -if getenv("TQDM_TELEGRAM_TOKEN") and getenv("TQDM_TELEGRAM_CHAT_ID"): +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 diff --git a/tqdm/contrib/discord.py b/tqdm/contrib/discord.py index 713a2f82..0edd35cc 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) @@ -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 new file mode 100644 index 00000000..b478d923 --- /dev/null +++ b/tqdm/contrib/slack.py @@ -0,0 +1,126 @@ +""" +Sends updates to a Slack app. + +Usage: +>>> from tqdm.contrib.slack import tqdm, trange +>>> for i in trange(10, 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", "casperdcl"]} +__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.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)) + self.message = None + + 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 + message = self.message + if message is None: + return + self.text = s + try: + 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: + 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:10}`').replace('{bar}', '`{bar:10u}`') + else: + 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): + 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