diff --git a/.coveragerc b/.coveragerc
index 6f24cb7a3..e7bff6bfe 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -2,5 +2,6 @@
branch = True
omit =
tqdm/tests/*
+ tqdm/contrib/telegram.py
[report]
show_missing = True
diff --git a/.meta/.readme.rst b/.meta/.readme.rst
index 707e2a4b3..510636c2b 100644
--- a/.meta/.readme.rst
+++ b/.meta/.readme.rst
@@ -419,10 +419,14 @@ Returns
def tqdm.contrib.tmap(function, *sequences, **tqdm_kwargs):
"""Equivalent of builtin `map`."""
+``contrib``
+-----------
+
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.telegram``: Posts to `Telegram `__ bots
Examples and Advanced Usage
---------------------------
diff --git a/README.rst b/README.rst
index 3d0015f4c..bf31c1717 100644
--- a/README.rst
+++ b/README.rst
@@ -605,10 +605,14 @@ Returns
def tqdm.contrib.tmap(function, *sequences, **tqdm_kwargs):
"""Equivalent of builtin `map`."""
+``contrib``
+-----------
+
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.telegram``: Posts to `Telegram `__ bots
Examples and Advanced Usage
---------------------------
diff --git a/tqdm/contrib/telegram.py b/tqdm/contrib/telegram.py
new file mode 100644
index 000000000..5654dc99c
--- /dev/null
+++ b/tqdm/contrib/telegram.py
@@ -0,0 +1,136 @@
+"""
+Sends updates to a Telegram bot.
+"""
+from __future__ import absolute_import
+
+from concurrent.futures import ThreadPoolExecutor
+from requests import Session
+
+from tqdm.auto import tqdm as tqdm_auto
+from tqdm.utils import _range
+__author__ = {"github.com/": ["casperdcl"]}
+__all__ = ['TelegramIO', 'tqdm_telegram', 'ttgrange', 'tqdm', 'trange']
+
+
+class TelegramIO():
+ """Non-blocking file-like IO to a Telegram Bot."""
+ API = 'https://api.telegram.org/bot'
+
+ def __init__(self, token, chat_id):
+ """Creates a new message in the given `chat_id`."""
+ self.token = token
+ self.chat_id = chat_id
+ self.session = session = Session()
+ self.text = self.__class__.__name__
+ self.pool = ThreadPoolExecutor()
+ self.futures = []
+ try:
+ res = session.post(
+ self.API + '%s/sendMessage' % self.token,
+ data=dict(text='`' + self.text + '`', chat_id=self.chat_id,
+ parse_mode='MarkdownV2'))
+ except Exception as e:
+ tqdm_auto.write(str(e))
+ else:
+ self.message_id = res.json()['result']['message_id']
+
+ def write(self, s):
+ """Replaces internal `message_id`'s text with `s`."""
+ if not s:
+ return
+ s = s.strip().replace('\r', '')
+ if s == self.text:
+ return # avoid duplicate message Bot error
+ self.text = s
+ try:
+ f = self.pool.submit(
+ self.session.post,
+ self.API + '%s/editMessageText' % self.token,
+ data=dict(
+ text='`' + s + '`', chat_id=self.chat_id,
+ message_id=self.message_id, parse_mode='MarkdownV2'))
+ except Exception as e:
+ tqdm_auto.write(str(e))
+ else:
+ self.futures.append(f)
+ return f
+
+ def flush(self):
+ """Ensure the last `write` has been processed."""
+ [f.cancel() for f in self.futures[-2::-1]]
+ try:
+ return self.futures[-1].result()
+ except IndexError:
+ pass
+ finally:
+ self.futures = []
+
+ def __del__(self):
+ self.flush()
+
+
+class tqdm_telegram(tqdm_auto):
+ """
+ Standard `tqdm.auto.tqdm` but also sends updates to a Telegram bot.
+ May take a few seconds to create (`__init__`) and clear (`__del__`).
+
+ >>> from tqdm.contrib.telegram import tqdm, trange
+ >>> for i in tqdm(
+ ... iterable,
+ ... token='1234567890:THIS1SSOMETOKEN0BTAINeDfrOmTELEGrAM',
+ ... chat_id='0246813579'):
+ """
+ def __init__(self, *args, **kwargs):
+ """
+ Parameters
+ ----------
+ token : str, required. Telegram token.
+ chat_id : str, required. Telegram chat ID.
+
+ See `tqdm.auto.tqdm.__init__` for other parameters.
+ """
+ self.tgio = TelegramIO(kwargs.pop('token'), kwargs.pop('chat_id'))
+ super(tqdm_telegram, self).__init__(*args, **kwargs)
+
+ def display(self, **kwargs):
+ super(tqdm_telegram, self).display(**kwargs)
+ fmt = self.format_dict
+ if 'bar_format' in fmt and fmt['bar_format']:
+ fmt['bar_format'] = fmt['bar_format'].replace('', '{bar}')
+ else:
+ fmt['bar_format'] = '{l_bar}{bar}{r_bar}'
+ fmt['bar_format'] = fmt['bar_format'].replace('{bar}', '{bar:10u}')
+ self.tgio.write(self.format_meter(**fmt))
+
+ def __new__(cls, *args, **kwargs):
+ """
+ Workaround for mixed-class same-stream nested progressbars.
+ See [#509](https://github.com/tqdm/tqdm/issues/509)
+ """
+ with cls.get_lock():
+ try:
+ cls._instances = tqdm_auto._instances
+ except AttributeError:
+ pass
+ instance = super(tqdm_telegram, cls).__new__(cls, *args, **kwargs)
+ with cls.get_lock():
+ try:
+ # `tqdm_auto` may have been changed so update
+ cls._instances.update(tqdm_auto._instances)
+ except AttributeError:
+ pass
+ tqdm_auto._instances = cls._instances
+ return instance
+
+
+def ttgrange(*args, **kwargs):
+ """
+ A shortcut for `tqdm.contrib.telegram.tqdm(xrange(*args), **kwargs)`.
+ On Python3+, `range` is used instead of `xrange`.
+ """
+ return tqdm_telegram(_range(*args), **kwargs)
+
+
+# Aliases
+tqdm = tqdm_telegram
+trange = ttgrange