diff --git a/.gitignore b/.gitignore index 4b3f0e8..3bf63b4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ .cache/ .idea/ .coverage -htmlcov/ \ No newline at end of file +htmlcov/ +venv/ +/python_telegram_handler.egg-info/ diff --git a/.travis.yml b/.travis.yml index cd6c9a8..c3a248f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,10 +2,10 @@ language: python cache: pip python: - - "2.7" - - "3.4" - - "3.5" - "3.6" + - "3.7" + - "3.8" + - "3.9" install: - pip install tox-travis codecov diff --git a/requirements-dev.txt b/requirements-dev.txt index c22fcf4..58dfb10 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,4 +2,5 @@ requests mock pytest pytest-cov -tox \ No newline at end of file +tox +python-telegram-bot \ No newline at end of file diff --git a/telegram_handler/TelegramBotQueue.py b/telegram_handler/TelegramBotQueue.py new file mode 100644 index 0000000..55e8b58 --- /dev/null +++ b/telegram_handler/TelegramBotQueue.py @@ -0,0 +1,24 @@ +import telegram +from telegram.ext import MessageQueue +from telegram.ext.messagequeue import queuedmessage + + +class MQBot(telegram.bot.Bot): + '''A subclass of Bot which delegates send method handling to MQ''' + def __init__(self, *args, **kwargs): + super(MQBot, self).__init__(*args, **kwargs) + # below 2 attributes should be provided for decorator usage + self._is_messages_queued_default = kwargs.get('is_queued_def', True) + self._msg_queue = kwargs.get('mqueue') or MessageQueue() + + def __del__(self): + try: + self._msg_queue.stop() + except: + pass + + @queuedmessage + def send_message(self, *args, **kwargs): + '''Wrapped method would accept new `queued` and `isgroup` + OPTIONAL arguments''' + return super(MQBot, self).send_message(*args, **kwargs) \ No newline at end of file diff --git a/telegram_handler/__init__.py b/telegram_handler/__init__.py index 9cb97d3..be62d98 100644 --- a/telegram_handler/__init__.py +++ b/telegram_handler/__init__.py @@ -1,5 +1,4 @@ -from telegram_handler.formatters import * -from telegram_handler.handlers import * +from telegram_handler.handlers import TelegramHandler def main(): # pragma: no cover diff --git a/telegram_handler/formatters.py b/telegram_handler/formatters.py index d63aaa9..fffbfeb 100644 --- a/telegram_handler/formatters.py +++ b/telegram_handler/formatters.py @@ -8,7 +8,8 @@ class TelegramFormatter(logging.Formatter): """Base formatter class suitable for use with `TelegramHandler`""" - fmt = "%(asctime)s %(levelname)s\n[%(name)s:%(funcName)s]\n%(message)s" + fmt = ("%(asctime)s %(levelname)s\n[%(host)s:%(name)s:%(funcName)s]\n%(" + "message)s") parse_mode = None def __init__(self, fmt=None, *args, **kwargs): @@ -17,7 +18,8 @@ def __init__(self, fmt=None, *args, **kwargs): class MarkdownFormatter(TelegramFormatter): """Markdown formatter for telegram.""" - fmt = '`%(asctime)s` *%(levelname)s*\n[%(name)s:%(funcName)s]\n%(message)s' + fmt = ('`%(asctime)s` *%(levelname)s*\n[%(host)s:%(name)s:%(' + 'funcName)s]\n%(message)s') parse_mode = 'Markdown' def formatException(self, *args, **kwargs): @@ -26,14 +28,17 @@ def formatException(self, *args, **kwargs): class EMOJI: - WHITE_CIRCLE = '\xE2\x9A\xAA' - BLUE_CIRCLE = '\xF0\x9F\x94\xB5' - RED_CIRCLE = '\xF0\x9F\x94\xB4' + WHITE_CIRCLE = '\U000026AA' + BLUE_CIRCLE = '\U0001F535' + YELLOW_CIRCLE = '\U0001f7e1' + RED_CIRCLE = '\U0001F534' + BLACK_CIRCLE = '\u26AB' class HtmlFormatter(TelegramFormatter): """HTML formatter for telegram.""" - fmt = '%(asctime)s %(levelname)s\nFrom %(name)s:%(funcName)s\n%(message)s' + fmt = ('%(asctime)s %(levelname)s\nFrom %(host)s:%(' + 'name)s:%(funcName)s\n%(message)s') parse_mode = 'HTML' def __init__(self, *args, **kwargs): @@ -52,13 +57,19 @@ def format(self, record): record.name = escape_html(str(record.name)) if record.msg: record.msg = escape_html(record.getMessage()) + if record.message: + record.message = escape_html(record.message) if self.use_emoji: if record.levelno == logging.DEBUG: record.levelname += ' ' + EMOJI.WHITE_CIRCLE elif record.levelno == logging.INFO: record.levelname += ' ' + EMOJI.BLUE_CIRCLE - else: + elif record.levelno == logging.WARNING: + record.levelname += ' ' + EMOJI.YELLOW_CIRCLE + elif record.levelno == logging.ERROR: record.levelname += ' ' + EMOJI.RED_CIRCLE + else: + record.levelname += ' ' + EMOJI.BLACK_CIRCLE if hasattr(self, '_style'): return self._style.format(record) diff --git a/telegram_handler/handlers.py b/telegram_handler/handlers.py index f471806..f2cf3e7 100644 --- a/telegram_handler/handlers.py +++ b/telegram_handler/handlers.py @@ -3,6 +3,7 @@ import requests +from telegram_handler.TelegramBotQueue import MQBot from telegram_handler.formatters import HtmlFormatter logger = logging.getLogger(__name__) @@ -19,14 +20,20 @@ class TelegramHandler(logging.Handler): API_ENDPOINT = 'https://api.telegram.org' last_response = None - def __init__(self, token, chat_id=None, level=logging.NOTSET, timeout=2, disable_notification=False, - disable_web_page_preview=False, proxies=None): + def __init__(self, token, chat_id=None, level=logging.WARNING, timeout=2, disable_notification=False, + disable_notification_logging_level=logging.ERROR, + disable_web_page_preview=False, proxies=None, **kwargs): self.token = token self.disable_web_page_preview = disable_web_page_preview + # self.disable_notification = kwargs.get('custom_disable_notification', disable_notification) self.disable_notification = disable_notification + if 'custom_enable_notification' in kwargs: + self.disable_notification = not kwargs.get('custom_enable_notification') + self.disable_notification_logging_level = disable_notification_logging_level self.timeout = timeout self.proxies = proxies self.chat_id = chat_id or self.get_chat_id() + level = kwargs.get('custom_logging_level', level) if not self.chat_id: level = logging.NOTSET logger.error('Did not get chat id. Setting handler logging level to NOTSET.') @@ -34,7 +41,8 @@ def __init__(self, token, chat_id=None, level=logging.NOTSET, timeout=2, disable super(TelegramHandler, self).__init__(level=level) - self.setFormatter(HtmlFormatter()) + self.setFormatter(HtmlFormatter(use_emoji=kwargs.get('use_emoji', True))) + self.bot = MQBot(token=self.token) @classmethod def format_url(cls, token, method): @@ -70,6 +78,7 @@ def request(self, method, **kwargs): return response + """ def send_message(self, text, **kwargs): data = {'text': text} data.update(kwargs) @@ -79,23 +88,42 @@ def send_document(self, text, document, **kwargs): data = {'caption': text} data.update(kwargs) return self.request('sendDocument', data=data, files={'document': ('traceback.txt', document, 'text/plain')}) + """ def emit(self, record): text = self.format(record) - + disable_notification = (record.levelno is None or record.levelno < self.disable_notification_logging_level) or \ + self.disable_notification data = { 'chat_id': self.chat_id, 'disable_web_page_preview': self.disable_web_page_preview, - 'disable_notification': self.disable_notification, + 'disable_notification': disable_notification, } if getattr(self.formatter, 'parse_mode', None): data['parse_mode'] = self.formatter.parse_mode - if len(text) < MAX_MESSAGE_LEN: - response = self.send_message(text, **data) - else: - response = self.send_document(text[:1000], document=BytesIO(text.encode()), **data) + kwargs = dict() + if self.timeout is not None: + kwargs.setdefault('timeout', self.timeout) + if self.proxies is not None: + kwargs.setdefault('proxies', self.proxies) + + - if response and not response.get('ok', False): - logger.warning('Telegram responded with ok=false status! {}'.format(response)) + try: + if len(text) < MAX_MESSAGE_LEN: + response = self.bot.send_message(text=text, api_kwargs=kwargs, **data) + else: + del data['disable_web_page_preview'] + response = self.bot.send_document(caption=text[:1000], api_kwargs=kwargs, document=BytesIO(text.encode()), + filename="traceback.txt", + **data) + if not response: + logger.warning( + 'Telegram responded with ok=false status! {}'.format( + response)) + except Exception as e: + logger.exception("Error while sending message to telegram, " + f"{str(e)}") + logger.debug(str(kwargs)) diff --git a/tests/test_formatters.py b/tests/test_formatters.py index 27aecf0..7ef4bc8 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -42,7 +42,9 @@ def test_html_formatter_emoji(): emoji_level_map = { formatters.EMOJI.WHITE_CIRCLE: [logging.DEBUG], formatters.EMOJI.BLUE_CIRCLE: [logging.INFO], - formatters.EMOJI.RED_CIRCLE: [logging.WARNING, logging.ERROR] + formatters.EMOJI.YELLOW_CIRCLE: [logging.WARNING], + formatters.EMOJI.RED_CIRCLE: [logging.ERROR], + formatters.EMOJI.BLACK_CIRCLE: [logging.CRITICAL] } for emoji, levels in emoji_level_map.items(): diff --git a/tests/test_handlers.py b/tests/test_handlers.py index 49e7a55..ca10a34 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -4,6 +4,10 @@ import mock import pytest import requests +from _pytest.monkeypatch import MonkeyPatch + +message_queue_patch = MonkeyPatch() +message_queue_patch.setattr("telegram_handler.TelegramBotQueue.MQBot.__init__", lambda *_, **kwargs: None) import telegram_handler.handlers @@ -52,21 +56,22 @@ def handler(): def test_emit(handler): record = logging.makeLogRecord({'msg': 'hello'}) - with mock.patch('requests.post') as patch: + with mock.patch('telegram_handler.TelegramBotQueue.MQBot.send_message') as patch: handler.emit(record) assert patch.called assert patch.call_count == 1 - assert patch.call_args[1]['json']['chat_id'] == 'bar' - assert 'hello' in patch.call_args[1]['json']['text'] - assert patch.call_args[1]['json']['parse_mode'] == 'HTML' + assert patch.call_args[1]['chat_id'] == 'bar' + assert 'hello' in patch.call_args[1]['text'] + assert patch.call_args[1]['parse_mode'] == 'HTML' + def test_emit_big_message(handler): message = '*' * telegram_handler.handlers.MAX_MESSAGE_LEN record = logging.makeLogRecord({'msg': message}) - with mock.patch('requests.post') as patch: + with mock.patch('telegram_handler.TelegramBotQueue.MQBot.send_document') as patch: handler.emit(record) assert patch.called @@ -76,25 +81,25 @@ def test_emit_big_message(handler): def test_emit_http_exception(handler): record = logging.makeLogRecord({'msg': 'hello'}) - with mock.patch('requests.post') as patch: - response = requests.Response() - response.status_code = 500 - response._content = 'Server error'.encode() - patch.return_value = response + with mock.patch('telegram_handler.TelegramBotQueue.MQBot.send_message') as patch: + # response = requests.Response() + # response.status_code = 500 + # response._content = 'Server error'.encode() + patch.return_value = None handler.emit(record) - assert telegram_handler.handlers.logger.handlers[0].messages['error'] - assert telegram_handler.handlers.logger.handlers[0].messages['debug'] + assert telegram_handler.handlers.logger.handlers[0].messages['warning'] + # assert telegram_handler.handlers.logger.handlers[0].messages['debug'] def test_emit_telegram_error(handler): record = logging.makeLogRecord({'msg': 'hello'}) - with mock.patch('requests.post') as patch: - response = requests.Response() - response.status_code = 200 - response._content = json.dumps({'ok': False}).encode() - patch.return_value = response + with mock.patch('telegram_handler.TelegramBotQueue.MQBot.send_message') as patch: + #response = requests.Response() + #response.status_code = 200 + #response._content = json.dumps({'ok': False}).encode() + patch.return_value = None handler.emit(record) assert telegram_handler.handlers.logger.handlers[0].messages['warning'] @@ -158,6 +163,7 @@ def test_handler_init_without_chat(): assert handler.level == logging.NOTSET + def test_handler_respects_proxy(): proxies = { 'http': 'http_proxy_sample', @@ -165,20 +171,21 @@ def test_handler_respects_proxy(): } handler = telegram_handler.handlers.TelegramHandler('foo', 'bar', level=logging.INFO, proxies=proxies) - + record = logging.makeLogRecord({'msg': 'hello'}) - with mock.patch('requests.post') as patch: + with mock.patch('telegram_handler.TelegramBotQueue.MQBot.send_message') as patch: handler.emit(record) - assert patch.call_args[1]['proxies'] == proxies + assert patch.call_args[1]['api_kwargs']['proxies'] == proxies + def test_custom_formatter(handler): handler.setFormatter(logging.Formatter()) record = logging.makeLogRecord({'msg': 'hello'}) - with mock.patch('requests.post') as patch: + with mock.patch('telegram_handler.TelegramBotQueue.MQBot.send_message') as patch: handler.emit(record) - assert 'parse_mode' not in patch.call_args[1]['json'] + assert 'parse_mode' not in patch.call_args[1] diff --git a/tox.ini b/tox.ini index ebece3b..8a13c84 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py30,py34,py35,py36,coverage +envlist = py36,py37,py38,py39,coverage [testenv] skip_install = True @@ -9,7 +9,7 @@ deps= commands=py.test tests --cov-fail-under 90 --color=auto --cov=telegram_handler --cov-report=term-missing [testenv:coverage] -basepython = python3.5 +basepython = python3.6 passenv = CI TRAVIS_BUILD_ID TRAVIS TRAVIS_BRANCH TRAVIS_JOB_NUMBER TRAVIS_PULL_REQUEST TRAVIS_JOB_ID TRAVIS_REPO_SLUG TRAVIS_COMMIT deps = codecov>=1.4.0