1
0
mirror of https://github.com/eternnoir/pyTelegramBotAPI.git synced 2023-08-10 21:12:57 +03:00

Merge pull request #1693 from coder2020official/conflicts

Logging improvements(still not 100%), file restarts on file changes(needs tests), and more
This commit is contained in:
Badiboy 2022-10-01 22:30:49 +03:00 committed by GitHub
commit 82ad37fed8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 243 additions and 12 deletions

View File

@ -0,0 +1,28 @@
#!/usr/bin/python
# This is a simple echo bot using the decorator mechanism.
# It echoes any incoming text messages.
from telebot.async_telebot import AsyncTeleBot
bot = AsyncTeleBot('TOKEN')
# Handle '/start' and '/help'
@bot.message_handler(commands=['help', 'start'])
async def send_welcome(message):
await bot.reply_to(message, """\
Hi there, I am EchoBot.
I am here to echo your kind words back to you. Just say anything nice and I'll say the exact same thing to you!\
""")
# Handle all other messages with content_type 'text' (content_types defaults to ['text'])
@bot.message_handler(func=lambda message: True)
async def echo_message(message):
await bot.reply_to(message, message.text)
import asyncio
# only new versions(4.7.0+)
asyncio.run(bot.polling(restart_on_change=True))

View File

@ -0,0 +1,28 @@
#!/usr/bin/python
# This is a simple echo bot using the decorator mechanism.
# It echoes any incoming text messages.
import telebot
API_TOKEN = '<api_token>'
bot = telebot.TeleBot(API_TOKEN)
# Handle '/start' and '/help'
@bot.message_handler(commands=['help', 'start'])
def send_welcome(message):
bot.reply_to(message, """\
Hi there, I am EchoBot.
I am here to echo your kind words back to you. Just say anything nice and I'll say the exact same thing to you!\
""")
# Handle all other messages with content_type 'text' (content_types defaults to ['text'])
@bot.message_handler(func=lambda message: True)
def echo_message(message):
bot.reply_to(message, message.text)
# only versions greater than 4.7.0
bot.infinity_polling(restart_on_change=True)

View File

@ -31,6 +31,9 @@ setup(name='pyTelegramBotAPI',
'aiohttp': 'aiohttp', 'aiohttp': 'aiohttp',
'fastapi': 'fastapi', 'fastapi': 'fastapi',
'uvicorn': 'uvicorn', 'uvicorn': 'uvicorn',
'psutil': 'psutil',
'coloredlogs': 'coloredlogs',
'watchdog': 'watchdog'
}, },
classifiers=[ classifiers=[
'Development Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',

View File

@ -95,6 +95,10 @@ class TeleBot:
See more examples in examples/ directory: See more examples in examples/ directory:
https://github.com/eternnoir/pyTelegramBotAPI/tree/master/examples https://github.com/eternnoir/pyTelegramBotAPI/tree/master/examples
.. note::
Install coloredlogs module to specify colorful_logs=True
:param token: Token of a bot, should be obtained from @BotFather :param token: Token of a bot, should be obtained from @BotFather
:type token: :obj:`str` :type token: :obj:`str`
@ -131,7 +135,7 @@ class TeleBot:
:param use_class_middlewares: Use class middlewares, defaults to False :param use_class_middlewares: Use class middlewares, defaults to False
:type use_class_middlewares: :obj:`bool`, optional :type use_class_middlewares: :obj:`bool`, optional
:param disable_web_page_preview: Default value for disable_web_page_preview, defaults to None :param disable_web_page_preview: Default value for disable_web_page_preview, defaults to None
:type disable_web_page_preview: :obj:`bool`, optional :type disable_web_page_preview: :obj:`bool`, optional
@ -143,6 +147,10 @@ class TeleBot:
:param allow_sending_without_reply: Default value for allow_sending_without_reply, defaults to None :param allow_sending_without_reply: Default value for allow_sending_without_reply, defaults to None
:type allow_sending_without_reply: :obj:`bool`, optional :type allow_sending_without_reply: :obj:`bool`, optional
:param colorful_logs: Outputs colorful logs
:type colorful_logs: :obj:`bool`, optional
""" """
def __init__( def __init__(
@ -155,7 +163,8 @@ class TeleBot:
disable_web_page_preview: Optional[bool]=None, disable_web_page_preview: Optional[bool]=None,
disable_notification: Optional[bool]=None, disable_notification: Optional[bool]=None,
protect_content: Optional[bool]=None, protect_content: Optional[bool]=None,
allow_sending_without_reply: Optional[bool]=None allow_sending_without_reply: Optional[bool]=None,
colorful_logs: Optional[bool]=False
): ):
# update-related # update-related
@ -171,6 +180,16 @@ class TeleBot:
self.protect_content = protect_content self.protect_content = protect_content
self.allow_sending_without_reply = allow_sending_without_reply self.allow_sending_without_reply = allow_sending_without_reply
# logs-related
if colorful_logs:
try:
import coloredlogs
coloredlogs.install(logger=logger, level=logger.level)
except ImportError:
raise ImportError(
'Install colorredlogs module to use colorful_logs option.'
)
# threading-related # threading-related
self.__stop_polling = threading.Event() self.__stop_polling = threading.Event()
self.exc_info = None self.exc_info = None
@ -865,12 +884,36 @@ class TeleBot:
for listener in self.update_listener: for listener in self.update_listener:
self._exec_task(listener, new_messages) self._exec_task(listener, new_messages)
def _setup_change_detector(self, path_to_watch: str):
try:
from watchdog.observers import Observer
from telebot.ext.reloader import EventHandler
except ImportError:
raise ImportError(
'Please install watchdog and psutil before using restart_on_change option.'
)
self.event_handler = EventHandler()
path = path_to_watch if path_to_watch else None
if path is None:
# Make it possible to specify --path argument to the script
path = sys.argv[sys.argv.index('--path') + 1] if '--path' in sys.argv else '.'
self.event_observer = Observer()
self.event_observer.schedule(self.event_handler, path, recursive=True)
self.event_observer.start()
def infinity_polling(self, timeout: Optional[int]=20, skip_pending: Optional[bool]=False, long_polling_timeout: Optional[int]=20, def infinity_polling(self, timeout: Optional[int]=20, skip_pending: Optional[bool]=False, long_polling_timeout: Optional[int]=20,
logger_level: Optional[int]=logging.ERROR, allowed_updates: Optional[List[str]]=None, *args, **kwargs): logger_level: Optional[int]=logging.ERROR, allowed_updates: Optional[List[str]]=None,
restart_on_change: Optional[bool]=False, path_to_watch: Optional[str]=None, *args, **kwargs):
""" """
Wrap polling with infinite loop and exception handling to avoid bot stops polling. Wrap polling with infinite loop and exception handling to avoid bot stops polling.
.. note::
Install watchdog and psutil before using restart_on_change option.
:param timeout: Request connection timeout. :param timeout: Request connection timeout.
:type timeout: :obj:`int` :type timeout: :obj:`int`
@ -893,15 +936,25 @@ class TeleBot:
so unwanted updates may be received for a short period of time. so unwanted updates may be received for a short period of time.
:type allowed_updates: :obj:`list` of :obj:`str` :type allowed_updates: :obj:`list` of :obj:`str`
:param restart_on_change: Restart a file on file(s) change. Defaults to False
:type restart_on_change: :obj:`bool`
:param path_to_watch: Path to watch for changes. Defaults to current directory
:type path_to_watch: :obj:`str`
:return: :return:
""" """
if skip_pending: if skip_pending:
self.__skip_updates() self.__skip_updates()
if restart_on_change:
self._setup_change_detector(path_to_watch)
while not self.__stop_polling.is_set(): while not self.__stop_polling.is_set():
try: try:
self.polling(non_stop=True, timeout=timeout, long_polling_timeout=long_polling_timeout, self.polling(non_stop=True, timeout=timeout, long_polling_timeout=long_polling_timeout,
logger_level=logger_level, allowed_updates=allowed_updates, *args, **kwargs) logger_level=logger_level, allowed_updates=allowed_updates, restart_on_change=False,
*args, **kwargs)
except Exception as e: except Exception as e:
if logger_level and logger_level >= logging.ERROR: if logger_level and logger_level >= logging.ERROR:
logger.error("Infinity polling exception: %s", str(e)) logger.error("Infinity polling exception: %s", str(e))
@ -918,7 +971,7 @@ class TeleBot:
def polling(self, non_stop: Optional[bool]=False, skip_pending: Optional[bool]=False, interval: Optional[int]=0, def polling(self, non_stop: Optional[bool]=False, skip_pending: Optional[bool]=False, interval: Optional[int]=0,
timeout: Optional[int]=20, long_polling_timeout: Optional[int]=20, timeout: Optional[int]=20, long_polling_timeout: Optional[int]=20,
logger_level: Optional[int]=logging.ERROR, allowed_updates: Optional[List[str]]=None, logger_level: Optional[int]=logging.ERROR, allowed_updates: Optional[List[str]]=None,
none_stop: Optional[bool]=None): none_stop: Optional[bool]=None, restart_on_change: Optional[bool]=False, path_to_watch: Optional[str]=None):
""" """
This function creates a new Thread that calls an internal __retrieve_updates function. This function creates a new Thread that calls an internal __retrieve_updates function.
This allows the bot to retrieve Updates automatically and notify listeners and message handlers accordingly. This allows the bot to retrieve Updates automatically and notify listeners and message handlers accordingly.
@ -930,6 +983,10 @@ class TeleBot:
.. deprecated:: 4.1.1 .. deprecated:: 4.1.1
Use :meth:`infinity_polling` instead. Use :meth:`infinity_polling` instead.
.. note::
Install watchdog and psutil before using restart_on_change option.
:param interval: Delay between two update retrivals :param interval: Delay between two update retrivals
:type interval: :obj:`int` :type interval: :obj:`int`
@ -961,6 +1018,12 @@ class TeleBot:
:param none_stop: Deprecated, use non_stop. Old typo, kept for backward compatibility. :param none_stop: Deprecated, use non_stop. Old typo, kept for backward compatibility.
:type none_stop: :obj:`bool` :type none_stop: :obj:`bool`
:param restart_on_change: Restart a file on file(s) change. Defaults to False
:type restart_on_change: :obj:`bool`
:param path_to_watch: Path to watch for changes. Defaults to None
:type path_to_watch: :obj:`str`
:return: :return:
""" """
@ -970,6 +1033,11 @@ class TeleBot:
if skip_pending: if skip_pending:
self.__skip_updates() self.__skip_updates()
if restart_on_change:
self._setup_change_detector(path_to_watch)
logger.info('Starting your bot with username: [@%s]', self.user.username)
if self.threaded: if self.threaded:
self.__threaded_polling(non_stop=non_stop, interval=interval, timeout=timeout, long_polling_timeout=long_polling_timeout, self.__threaded_polling(non_stop=non_stop, interval=interval, timeout=timeout, long_polling_timeout=long_polling_timeout,

View File

@ -5,6 +5,7 @@ import logging
import re import re
import traceback import traceback
from typing import Any, Awaitable, Callable, List, Optional, Union from typing import Any, Awaitable, Callable, List, Optional, Union
import sys
# this imports are used to avoid circular import error # this imports are used to avoid circular import error
import telebot.util import telebot.util
@ -17,12 +18,11 @@ from telebot.asyncio_handler_backends import BaseMiddleware, CancelUpdate, SkipH
from inspect import signature from inspect import signature
from telebot import logger
from telebot import util, types, asyncio_helper from telebot import util, types, asyncio_helper
import asyncio import asyncio
from telebot import asyncio_filters from telebot import asyncio_filters
logger = logging.getLogger('TeleBot')
REPLY_MARKUP_TYPES = Union[ REPLY_MARKUP_TYPES = Union[
types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup, types.InlineKeyboardMarkup, types.ReplyKeyboardMarkup,
@ -82,6 +82,10 @@ class AsyncTeleBot:
See more examples in examples/ directory: See more examples in examples/ directory:
https://github.com/eternnoir/pyTelegramBotAPI/tree/master/examples https://github.com/eternnoir/pyTelegramBotAPI/tree/master/examples
.. note::
Install coloredlogs module to specify colorful_logs=True
:param token: Token of a bot, obtained from @BotFather :param token: Token of a bot, obtained from @BotFather
:type token: :obj:`str` :type token: :obj:`str`
@ -109,6 +113,9 @@ class AsyncTeleBot:
:param allow_sending_without_reply: Default value for allow_sending_without_reply, defaults to None :param allow_sending_without_reply: Default value for allow_sending_without_reply, defaults to None
:type allow_sending_without_reply: :obj:`bool`, optional :type allow_sending_without_reply: :obj:`bool`, optional
:param colorful_logs: Outputs colorful logs
:type colorful_logs: :obj:`bool`, optional
""" """
@ -118,12 +125,23 @@ class AsyncTeleBot:
disable_web_page_preview: Optional[bool]=None, disable_web_page_preview: Optional[bool]=None,
disable_notification: Optional[bool]=None, disable_notification: Optional[bool]=None,
protect_content: Optional[bool]=None, protect_content: Optional[bool]=None,
allow_sending_without_reply: Optional[bool]=None) -> None: allow_sending_without_reply: Optional[bool]=None,
colorful_logs: Optional[bool]=False) -> None:
# update-related # update-related
self.token = token self.token = token
self.offset = offset self.offset = offset
# logs-related
if colorful_logs:
try:
import coloredlogs
coloredlogs.install(logger=logger, level=logger.level)
except ImportError:
raise ImportError(
'Install colorredlogs module to use colorful_logs option.'
)
# properties # properties
self.parse_mode = parse_mode self.parse_mode = parse_mode
self.disable_web_page_preview = disable_web_page_preview self.disable_web_page_preview = disable_web_page_preview
@ -155,6 +173,12 @@ class AsyncTeleBot:
self.state_handlers = [] self.state_handlers = []
self.middlewares = [] self.middlewares = []
self._user = None # set during polling
@property
def user(self):
return self._user
async def close_session(self): async def close_session(self):
""" """
Closes existing session of aiohttp. Closes existing session of aiohttp.
@ -194,9 +218,28 @@ class AsyncTeleBot:
json_updates = await asyncio_helper.get_updates(self.token, offset, limit, timeout, allowed_updates, request_timeout) json_updates = await asyncio_helper.get_updates(self.token, offset, limit, timeout, allowed_updates, request_timeout)
return [types.Update.de_json(ju) for ju in json_updates] return [types.Update.de_json(ju) for ju in json_updates]
def _setup_change_detector(self, path_to_watch: str) -> None:
try:
from watchdog.observers import Observer
from telebot.ext.reloader import EventHandler
except ImportError:
raise ImportError(
'Please install watchdog and psutil before using restart_on_change option.'
)
self.event_handler = EventHandler()
path = path_to_watch if path_to_watch else None
if path is None:
# Make it possible to specify --path argument to the script
path = sys.argv[sys.argv.index('--path') + 1] if '--path' in sys.argv else '.'
self.event_observer = Observer()
self.event_observer.schedule(self.event_handler, path, recursive=True)
self.event_observer.start()
async def polling(self, non_stop: bool=False, skip_pending=False, interval: int=0, timeout: int=20, async def polling(self, non_stop: bool=False, skip_pending=False, interval: int=0, timeout: int=20,
request_timeout: Optional[int]=None, allowed_updates: Optional[List[str]]=None, request_timeout: Optional[int]=None, allowed_updates: Optional[List[str]]=None,
none_stop: Optional[bool]=None): none_stop: Optional[bool]=None, restart_on_change: Optional[bool]=False, path_to_watch: Optional[str]=None):
""" """
Runs bot in long-polling mode in a main loop. Runs bot in long-polling mode in a main loop.
This allows the bot to retrieve Updates automagically and notify listeners and message handlers accordingly. This allows the bot to retrieve Updates automagically and notify listeners and message handlers accordingly.
@ -210,6 +253,10 @@ class AsyncTeleBot:
Set non_stop=True if you want your bot to continue receiving updates Set non_stop=True if you want your bot to continue receiving updates
if there is an error. if there is an error.
.. note::
Install watchdog and psutil before using restart_on_change option.
:param non_stop: Do not stop polling when an ApiException occurs. :param non_stop: Do not stop polling when an ApiException occurs.
:type non_stop: :obj:`bool` :type non_stop: :obj:`bool`
@ -237,6 +284,12 @@ class AsyncTeleBot:
:param none_stop: Deprecated, use non_stop. Old typo, kept for backward compatibility. :param none_stop: Deprecated, use non_stop. Old typo, kept for backward compatibility.
:type none_stop: :obj:`bool` :type none_stop: :obj:`bool`
:param restart_on_change: Restart a file on file(s) change. Defaults to False.
:type restart_on_change: :obj:`bool`
:param path_to_watch: Path to watch for changes. Defaults to current directory
:type path_to_watch: :obj:`str`
:return: :return:
""" """
@ -246,13 +299,21 @@ class AsyncTeleBot:
if skip_pending: if skip_pending:
await self.skip_updates() await self.skip_updates()
if restart_on_change:
self._setup_change_detector(path_to_watch)
await self._process_polling(non_stop, interval, timeout, request_timeout, allowed_updates) await self._process_polling(non_stop, interval, timeout, request_timeout, allowed_updates)
async def infinity_polling(self, timeout: Optional[int]=20, skip_pending: Optional[bool]=False, request_timeout: Optional[int]=None, async def infinity_polling(self, timeout: Optional[int]=20, skip_pending: Optional[bool]=False, request_timeout: Optional[int]=None,
logger_level: Optional[int]=logging.ERROR, allowed_updates: Optional[List[str]]=None, *args, **kwargs): logger_level: Optional[int]=logging.ERROR, allowed_updates: Optional[List[str]]=None,
restart_on_change: Optional[bool]=False, path_to_watch: Optional[str]=None, *args, **kwargs):
""" """
Wrap polling with infinite loop and exception handling to avoid bot stops polling. Wrap polling with infinite loop and exception handling to avoid bot stops polling.
.. note::
Install watchdog and psutil before using restart_on_change option.
:param timeout: Timeout in seconds for get_updates(Defaults to None) :param timeout: Timeout in seconds for get_updates(Defaults to None)
:type timeout: :obj:`int` :type timeout: :obj:`int`
@ -276,11 +337,21 @@ class AsyncTeleBot:
so unwanted updates may be received for a short period of time. so unwanted updates may be received for a short period of time.
:type allowed_updates: :obj:`list` of :obj:`str` :type allowed_updates: :obj:`list` of :obj:`str`
:param restart_on_change: Restart a file on file(s) change. Defaults to False
:type restart_on_change: :obj:`bool`
:param path_to_watch: Path to watch for changes. Defaults to current directory
:type path_to_watch: :obj:`str`
:return: None :return: None
""" """
if skip_pending: if skip_pending:
await self.skip_updates() await self.skip_updates()
self._polling = True self._polling = True
if restart_on_change:
self._setup_change_detector(path_to_watch)
while self._polling: while self._polling:
try: try:
await self._process_polling(non_stop=False, timeout=timeout, request_timeout=request_timeout, await self._process_polling(non_stop=False, timeout=timeout, request_timeout=request_timeout,
@ -314,9 +385,15 @@ class AsyncTeleBot:
Please note that this parameter doesn't affect updates created before the call to the get_updates, Please note that this parameter doesn't affect updates created before the call to the get_updates,
so unwanted updates may be received for a short period of time. so unwanted updates may be received for a short period of time.
:return: :return:
""" """
self._user = await self.get_me()
logger.info('Starting your bot with username: [@%s]', self.user.username)
self._polling = True self._polling = True
try: try:

View File

@ -13,8 +13,10 @@ API_URL = 'https://api.telegram.org/bot{0}/{1}'
from datetime import datetime from datetime import datetime
from telebot import util, logger from telebot import util
import logging
logger = logging.getLogger('TeleBot')
proxy = None proxy = None
session = None session = None
@ -69,7 +71,7 @@ async def _process_request(token, url, method='get', params=None, files=None, **
else: else:
# let's check for timeout in params # let's check for timeout in params
request_timeout = params.pop('timeout', None) request_timeout = params.pop('timeout', None) if params else None
# we will apply default request_timeout if there is no timeout in params # we will apply default request_timeout if there is no timeout in params
# otherwise, we will use timeout parameter applied for payload. # otherwise, we will use timeout parameter applied for payload.

25
telebot/ext/reloader.py Normal file
View File

@ -0,0 +1,25 @@
from watchdog.events import FileSystemEventHandler
from watchdog.events import FileSystemEvent
import psutil
import os
import sys
import logging
logger = logging.getLogger('TeleBot')
class EventHandler(FileSystemEventHandler):
def on_any_event(self, event: FileSystemEvent):
logger.info('* Detected changes in: %s, reloading', (event.src_path))
restart_file()
def restart_file():
try:
p = psutil.Process(os.getpid())
for handler in p.open_files() + p.connections():
os.close(handler.fd)
except Exception as e:
logger.error(e)
python = sys.executable
os.execl(python, python, *sys.argv)