mirror of
https://github.com/aiogram/aiogram.git
synced 2026-04-08 16:37:47 +00:00
Merge remote-tracking branch 'origin/dev-3.x' into dev-3.x
# Conflicts: # README.md # README.rst # aiogram/__init__.py # aiogram/bot/bot.py # aiogram/contrib/fsm_storage/redis.py # aiogram/contrib/middlewares/logging.py # aiogram/dispatcher/dispatcher.py # aiogram/dispatcher/filters/__init__.py # aiogram/dispatcher/filters/builtin.py # aiogram/dispatcher/filters/filters.py # aiogram/dispatcher/filters/state.py # aiogram/dispatcher/handler.py # aiogram/dispatcher/webhook.py # aiogram/types/base.py # aiogram/types/chat.py # aiogram/types/chat_member.py # aiogram/types/input_media.py # aiogram/types/message.py # aiogram/utils/callback_data.py # aiogram/utils/deprecated.py # aiogram/utils/exceptions.py # aiogram/utils/executor.py # aiogram/utils/helper.py # aiogram/utils/json.py # aiogram/utils/mixins.py # aiogram/utils/payload.py # dev_requirements.txt # docs/source/index.rst # examples/callback_data_factory.py # examples/check_user_language.py # examples/echo_bot.py # examples/finite_state_machine_example.py # examples/i18n_example.py # examples/inline_bot.py # examples/media_group.py # examples/middleware_and_antiflood.py # examples/payments.py # examples/proxy_and_emojize.py # examples/regexp_commands_filter_example.py # examples/throtling_example.py # examples/webhook_example.py # examples/webhook_example_2.py # setup.py # tests/test_bot.py # tests/test_token.py # tests/types/dataset.py
This commit is contained in:
commit
87393f2475
98 changed files with 5048 additions and 4854 deletions
|
|
@ -8,7 +8,10 @@ import collections
|
|||
import hashlib
|
||||
import hmac
|
||||
|
||||
from aiogram.utils.deprecated import deprecated
|
||||
|
||||
|
||||
@deprecated('`generate_hash` is outdated, please use `check_signature` or `check_integrity`', stacklevel=3)
|
||||
def generate_hash(data: dict, token: str) -> str:
|
||||
"""
|
||||
Generate secret hash
|
||||
|
|
@ -24,6 +27,7 @@ def generate_hash(data: dict, token: str) -> str:
|
|||
return hmac.new(secret.digest(), msg.encode("utf-8"), digestmod=hashlib.sha256).hexdigest()
|
||||
|
||||
|
||||
@deprecated('`check_token` helper was renamed to `check_integrity`', stacklevel=3)
|
||||
def check_token(data: dict, token: str) -> bool:
|
||||
"""
|
||||
Validate auth token
|
||||
|
|
@ -34,3 +38,32 @@ def check_token(data: dict, token: str) -> bool:
|
|||
"""
|
||||
param_hash = data.get("hash", "") or ""
|
||||
return param_hash == generate_hash(data, token)
|
||||
|
||||
|
||||
def check_signature(token: str, hash: str, **kwargs) -> bool:
|
||||
"""
|
||||
Generate hexadecimal representation
|
||||
of the HMAC-SHA-256 signature of the data-check-string
|
||||
with the SHA256 hash of the bot's token used as a secret key
|
||||
|
||||
:param token:
|
||||
:param hash:
|
||||
:param kwargs: all params received on auth
|
||||
:return:
|
||||
"""
|
||||
secret = hashlib.sha256(token.encode('utf-8'))
|
||||
check_string = '\n'.join(map(lambda k: f'{k}={kwargs[k]}', sorted(kwargs)))
|
||||
hmac_string = hmac.new(secret.digest(), check_string.encode('utf-8'), digestmod=hashlib.sha256).hexdigest()
|
||||
return hmac_string == hash
|
||||
|
||||
|
||||
def check_integrity(token: str, data: dict) -> bool:
|
||||
"""
|
||||
Verify the authentication and the integrity
|
||||
of the data received on user's auth
|
||||
|
||||
:param token: Bot's token
|
||||
:param data: all data that came on auth
|
||||
:return:
|
||||
"""
|
||||
return check_signature(token, **data)
|
||||
|
|
|
|||
|
|
@ -26,15 +26,15 @@ class CallbackData:
|
|||
Callback data factory
|
||||
"""
|
||||
|
||||
def __init__(self, prefix, *parts, sep=":"):
|
||||
def __init__(self, prefix, *parts, sep=':'):
|
||||
if not isinstance(prefix, str):
|
||||
raise TypeError(f"Prefix must be instance of str not {type(prefix).__name__}")
|
||||
elif not prefix:
|
||||
raise TypeError(f'Prefix must be instance of str not {type(prefix).__name__}')
|
||||
if not prefix:
|
||||
raise ValueError("Prefix can't be empty")
|
||||
elif sep in prefix:
|
||||
raise ValueError(f"Separator '{sep}' can't be used in prefix")
|
||||
elif not parts:
|
||||
raise TypeError("Parts is not passed!")
|
||||
if sep in prefix:
|
||||
raise ValueError(f"Separator {sep!r} can't be used in prefix")
|
||||
if not parts:
|
||||
raise TypeError('Parts were not passed!')
|
||||
|
||||
self.prefix = prefix
|
||||
self.sep = sep
|
||||
|
|
@ -59,24 +59,24 @@ class CallbackData:
|
|||
if args:
|
||||
value = args.pop(0)
|
||||
else:
|
||||
raise ValueError(f"Value for '{part}' is not passed!")
|
||||
raise ValueError(f'Value for {part!r} was not passed!')
|
||||
|
||||
if value is not None and not isinstance(value, str):
|
||||
value = str(value)
|
||||
|
||||
if not value:
|
||||
raise ValueError(f"Value for part {part} can't be empty!'")
|
||||
elif self.sep in value:
|
||||
raise ValueError(f"Symbol defined as separator can't be used in values of parts")
|
||||
raise ValueError(f"Value for part {part!r} can't be empty!'")
|
||||
if self.sep in value:
|
||||
raise ValueError(f"Symbol {self.sep!r} is defined as the separator and can't be used in parts' values")
|
||||
|
||||
data.append(value)
|
||||
|
||||
if args or kwargs:
|
||||
raise TypeError("Too many arguments is passed!")
|
||||
raise TypeError('Too many arguments were passed!')
|
||||
|
||||
callback_data = self.sep.join(data)
|
||||
if len(callback_data) > 64:
|
||||
raise ValueError("Resulted callback data is too long!")
|
||||
raise ValueError('Resulted callback data is too long!')
|
||||
|
||||
return callback_data
|
||||
|
||||
|
|
@ -91,9 +91,9 @@ class CallbackData:
|
|||
if prefix != self.prefix:
|
||||
raise ValueError("Passed callback data can't be parsed with that prefix.")
|
||||
elif len(parts) != len(self._part_names):
|
||||
raise ValueError("Invalid parts count!")
|
||||
raise ValueError('Invalid parts count!')
|
||||
|
||||
result = {"@": prefix}
|
||||
result = {'@': prefix}
|
||||
result.update(zip(self._part_names, parts))
|
||||
return result
|
||||
|
||||
|
|
@ -106,11 +106,12 @@ class CallbackData:
|
|||
"""
|
||||
for key in config.keys():
|
||||
if key not in self._part_names:
|
||||
raise ValueError(f"Invalid field name '{key}'")
|
||||
raise ValueError(f'Invalid field name {key!r}')
|
||||
return CallbackDataFilter(self, config)
|
||||
|
||||
|
||||
class CallbackDataFilter(Filter):
|
||||
|
||||
def __init__(self, factory: CallbackData, config: typing.Dict[str, str]):
|
||||
self.config = config
|
||||
self.factory = factory
|
||||
|
|
@ -124,12 +125,12 @@ class CallbackDataFilter(Filter):
|
|||
data = self.factory.parse(query.data)
|
||||
except ValueError:
|
||||
return False
|
||||
else:
|
||||
for key, value in self.config.items():
|
||||
if isinstance(value, (list, tuple, set)):
|
||||
if data.get(key) not in value:
|
||||
return False
|
||||
else:
|
||||
if value != data.get(key):
|
||||
return False
|
||||
return {"callback_data": data}
|
||||
|
||||
for key, value in self.config.items():
|
||||
if isinstance(value, (list, tuple, set, frozenset)):
|
||||
if data.get(key) not in value:
|
||||
return False
|
||||
else:
|
||||
if data.get(key) != value:
|
||||
return False
|
||||
return {'callback_data': data}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
"""
|
||||
Source: https://stackoverflow.com/questions/2536307/decorators-in-the-python-standard-lib-deprecated-specifically
|
||||
"""
|
||||
|
||||
import functools
|
||||
import asyncio
|
||||
import inspect
|
||||
import warnings
|
||||
import functools
|
||||
from typing import Callable
|
||||
|
||||
|
||||
def deprecated(reason):
|
||||
def deprecated(reason, stacklevel=2) -> Callable:
|
||||
"""
|
||||
This is a decorator which can be used to mark functions
|
||||
as deprecated. It will result in a warning being emitted
|
||||
when the function is used.
|
||||
|
||||
Source: https://stackoverflow.com/questions/2536307/decorators-in-the-python-standard-lib-deprecated-specifically
|
||||
"""
|
||||
|
||||
if isinstance(reason, str):
|
||||
|
|
@ -33,15 +33,15 @@ def deprecated(reason):
|
|||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
warn_deprecated(msg.format(name=func.__name__, reason=reason))
|
||||
warnings.simplefilter("default", DeprecationWarning)
|
||||
warn_deprecated(msg.format(name=func.__name__, reason=reason), stacklevel=stacklevel)
|
||||
warnings.simplefilter('default', DeprecationWarning)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
elif inspect.isclass(reason) or inspect.isfunction(reason):
|
||||
if inspect.isclass(reason) or inspect.isfunction(reason):
|
||||
|
||||
# The @deprecated is used without any 'reason'.
|
||||
#
|
||||
|
|
@ -60,16 +60,76 @@ def deprecated(reason):
|
|||
|
||||
@functools.wraps(func1)
|
||||
def wrapper1(*args, **kwargs):
|
||||
warn_deprecated(msg1.format(name=func1.__name__))
|
||||
warn_deprecated(msg1.format(name=func1.__name__), stacklevel=stacklevel)
|
||||
return func1(*args, **kwargs)
|
||||
|
||||
return wrapper1
|
||||
|
||||
else:
|
||||
raise TypeError(repr(type(reason)))
|
||||
raise TypeError(repr(type(reason)))
|
||||
|
||||
|
||||
def warn_deprecated(message, warning=DeprecationWarning, stacklevel=2):
|
||||
warnings.simplefilter("always", warning)
|
||||
warnings.simplefilter('always', warning)
|
||||
warnings.warn(message, category=warning, stacklevel=stacklevel)
|
||||
warnings.simplefilter("default", warning)
|
||||
warnings.simplefilter('default', warning)
|
||||
|
||||
|
||||
def renamed_argument(old_name: str, new_name: str, until_version: str, stacklevel: int = 3):
|
||||
"""
|
||||
A meta-decorator to mark an argument as deprecated.
|
||||
|
||||
.. code-block:: python3
|
||||
|
||||
@renamed_argument("chat", "chat_id", "3.0") # stacklevel=3 by default
|
||||
@renamed_argument("user", "user_id", "3.0", stacklevel=4)
|
||||
def some_function(user_id, chat_id=None):
|
||||
print(f"user_id={user_id}, chat_id={chat_id}")
|
||||
|
||||
some_function(user=123) # prints 'user_id=123, chat_id=None' with warning
|
||||
some_function(123) # prints 'user_id=123, chat_id=None' without warning
|
||||
some_function(user_id=123) # prints 'user_id=123, chat_id=None' without warning
|
||||
|
||||
|
||||
:param old_name:
|
||||
:param new_name:
|
||||
:param until_version: the version in which the argument is scheduled to be removed
|
||||
:param stacklevel: leave it to default if it's the first decorator used.
|
||||
Increment with any new decorator used.
|
||||
:return: decorator
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
@functools.wraps(func)
|
||||
async def wrapped(*args, **kwargs):
|
||||
if old_name in kwargs:
|
||||
warn_deprecated(f"In coroutine '{func.__name__}' argument '{old_name}' "
|
||||
f"is renamed to '{new_name}' "
|
||||
f"and will be removed in aiogram {until_version}",
|
||||
stacklevel=stacklevel)
|
||||
kwargs.update(
|
||||
{
|
||||
new_name: kwargs[old_name],
|
||||
}
|
||||
)
|
||||
kwargs.pop(old_name)
|
||||
return await func(*args, **kwargs)
|
||||
else:
|
||||
@functools.wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
if old_name in kwargs:
|
||||
warn_deprecated(f"In function `{func.__name__}` argument `{old_name}` "
|
||||
f"is renamed to `{new_name}` "
|
||||
f"and will be removed in aiogram {until_version}",
|
||||
stacklevel=stacklevel)
|
||||
kwargs.update(
|
||||
{
|
||||
new_name: kwargs[old_name],
|
||||
}
|
||||
)
|
||||
kwargs.pop(old_name)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
return decorator
|
||||
|
|
|
|||
|
|
@ -1,102 +1,100 @@
|
|||
"""
|
||||
TelegramAPIError
|
||||
ValidationError
|
||||
Throttled
|
||||
BadRequest
|
||||
MessageError
|
||||
MessageNotModified
|
||||
MessageToForwardNotFound
|
||||
MessageToDeleteNotFound
|
||||
MessageIdentifierNotSpecified
|
||||
MessageTextIsEmpty
|
||||
MessageCantBeEdited
|
||||
MessageCantBeDeleted
|
||||
MessageToEditNotFound
|
||||
MessageToReplyNotFound
|
||||
ToMuchMessages
|
||||
PollError
|
||||
PollCantBeStopped
|
||||
PollHasAlreadyClosed
|
||||
PollsCantBeSentToPrivateChats
|
||||
PollSizeError
|
||||
PollMustHaveMoreOptions
|
||||
PollCantHaveMoreOptions
|
||||
PollsOptionsLengthTooLong
|
||||
PollOptionsMustBeNonEmpty
|
||||
PollQuestionMustBeNonEmpty
|
||||
MessageWithPollNotFound (with MessageError)
|
||||
MessageIsNotAPoll (with MessageError)
|
||||
ObjectExpectedAsReplyMarkup
|
||||
InlineKeyboardExpected
|
||||
ChatNotFound
|
||||
ChatDescriptionIsNotModified
|
||||
InvalidQueryID
|
||||
InvalidPeerID
|
||||
InvalidHTTPUrlContent
|
||||
ButtonURLInvalid
|
||||
URLHostIsEmpty
|
||||
StartParamInvalid
|
||||
ButtonDataInvalid
|
||||
WrongFileIdentifier
|
||||
GroupDeactivated
|
||||
BadWebhook
|
||||
WebhookRequireHTTPS
|
||||
BadWebhookPort
|
||||
BadWebhookAddrInfo
|
||||
BadWebhookNoAddressAssociatedWithHostname
|
||||
NotFound
|
||||
MethodNotKnown
|
||||
PhotoAsInputFileRequired
|
||||
InvalidStickersSet
|
||||
NoStickerInRequest
|
||||
ChatAdminRequired
|
||||
NeedAdministratorRightsInTheChannel
|
||||
MethodNotAvailableInPrivateChats
|
||||
CantDemoteChatCreator
|
||||
CantRestrictSelf
|
||||
NotEnoughRightsToRestrict
|
||||
PhotoDimensions
|
||||
UnavailableMembers
|
||||
TypeOfFileMismatch
|
||||
WrongRemoteFileIdSpecified
|
||||
PaymentProviderInvalid
|
||||
CurrencyTotalAmountInvalid
|
||||
CantParseUrl
|
||||
UnsupportedUrlProtocol
|
||||
CantParseEntities
|
||||
ResultIdDuplicate
|
||||
ConflictError
|
||||
TerminatedByOtherGetUpdates
|
||||
CantGetUpdates
|
||||
Unauthorized
|
||||
BotKicked
|
||||
BotBlocked
|
||||
UserDeactivated
|
||||
CantInitiateConversation
|
||||
CantTalkWithBots
|
||||
NetworkError
|
||||
RetryAfter
|
||||
MigrateToChat
|
||||
RestartingTelegram
|
||||
- TelegramAPIError
|
||||
- ValidationError
|
||||
- Throttled
|
||||
- BadRequest
|
||||
- MessageError
|
||||
- MessageNotModified
|
||||
- MessageToForwardNotFound
|
||||
- MessageToDeleteNotFound
|
||||
- MessageIdentifierNotSpecified
|
||||
- MessageTextIsEmpty
|
||||
- MessageCantBeEdited
|
||||
- MessageCantBeDeleted
|
||||
- MessageToEditNotFound
|
||||
- MessageToReplyNotFound
|
||||
- ToMuchMessages
|
||||
- PollError
|
||||
- PollCantBeStopped
|
||||
- PollHasAlreadyClosed
|
||||
- PollsCantBeSentToPrivateChats
|
||||
- PollSizeError
|
||||
- PollMustHaveMoreOptions
|
||||
- PollCantHaveMoreOptions
|
||||
- PollsOptionsLengthTooLong
|
||||
- PollOptionsMustBeNonEmpty
|
||||
- PollQuestionMustBeNonEmpty
|
||||
- MessageWithPollNotFound (with MessageError)
|
||||
- MessageIsNotAPoll (with MessageError)
|
||||
- ObjectExpectedAsReplyMarkup
|
||||
- InlineKeyboardExpected
|
||||
- ChatNotFound
|
||||
- ChatDescriptionIsNotModified
|
||||
- InvalidQueryID
|
||||
- InvalidPeerID
|
||||
- InvalidHTTPUrlContent
|
||||
- ButtonURLInvalid
|
||||
- URLHostIsEmpty
|
||||
- StartParamInvalid
|
||||
- ButtonDataInvalid
|
||||
- WrongFileIdentifier
|
||||
- GroupDeactivated
|
||||
- BadWebhook
|
||||
- WebhookRequireHTTPS
|
||||
- BadWebhookPort
|
||||
- BadWebhookAddrInfo
|
||||
- BadWebhookNoAddressAssociatedWithHostname
|
||||
- NotFound
|
||||
- MethodNotKnown
|
||||
- PhotoAsInputFileRequired
|
||||
- InvalidStickersSet
|
||||
- NoStickerInRequest
|
||||
- ChatAdminRequired
|
||||
- NeedAdministratorRightsInTheChannel
|
||||
- MethodNotAvailableInPrivateChats
|
||||
- CantDemoteChatCreator
|
||||
- CantRestrictSelf
|
||||
- NotEnoughRightsToRestrict
|
||||
- PhotoDimensions
|
||||
- UnavailableMembers
|
||||
- TypeOfFileMismatch
|
||||
- WrongRemoteFileIdSpecified
|
||||
- PaymentProviderInvalid
|
||||
- CurrencyTotalAmountInvalid
|
||||
- CantParseUrl
|
||||
- UnsupportedUrlProtocol
|
||||
- CantParseEntities
|
||||
- ResultIdDuplicate
|
||||
- ConflictError
|
||||
- TerminatedByOtherGetUpdates
|
||||
- CantGetUpdates
|
||||
- Unauthorized
|
||||
- BotKicked
|
||||
- BotBlocked
|
||||
- UserDeactivated
|
||||
- CantInitiateConversation
|
||||
- CantTalkWithBots
|
||||
- NetworkError
|
||||
- RetryAfter
|
||||
- MigrateToChat
|
||||
- RestartingTelegram
|
||||
|
||||
|
||||
TODO: aiogram.utils.exceptions.BadRequest: Bad request: can't parse entities: unsupported start tag "function" at byte offset 0
|
||||
TODO: aiogram.utils.exceptions.TelegramAPIError: Gateway Timeout
|
||||
|
||||
AIOGramWarning
|
||||
TimeoutWarning
|
||||
- AIOGramWarning
|
||||
- TimeoutWarning
|
||||
"""
|
||||
import time
|
||||
|
||||
# TODO: Use exceptions detector from `aiograph`.
|
||||
# TODO: aiogram.utils.exceptions.BadRequest: Bad request: can't parse entities: unsupported start tag "function" at byte offset 0
|
||||
# TODO: aiogram.utils.exceptions.TelegramAPIError: Gateway Timeout
|
||||
|
||||
_PREFIXES = ["error: ", "[error]: ", "bad request: ", "conflict: ", "not found: "]
|
||||
_PREFIXES = ['error: ', '[error]: ', 'bad request: ', 'conflict: ', 'not found: ']
|
||||
|
||||
|
||||
def _clean_message(text):
|
||||
for prefix in _PREFIXES:
|
||||
if text.startswith(prefix):
|
||||
text = text[len(prefix) :]
|
||||
text = text[len(prefix):]
|
||||
return (text[0].upper() + text[1:]).strip()
|
||||
|
||||
|
||||
|
|
@ -106,7 +104,7 @@ class TelegramAPIError(Exception):
|
|||
|
||||
|
||||
class _MatchErrorMixin:
|
||||
match = ""
|
||||
match = ''
|
||||
text = None
|
||||
|
||||
__subclasses = []
|
||||
|
|
@ -166,72 +164,67 @@ class MessageNotModified(MessageError):
|
|||
"""
|
||||
Will be raised when you try to set new text is equals to current text.
|
||||
"""
|
||||
|
||||
match = "message is not modified"
|
||||
match = 'message is not modified'
|
||||
|
||||
|
||||
class MessageToForwardNotFound(MessageError):
|
||||
"""
|
||||
Will be raised when you try to forward very old or deleted or unknown message.
|
||||
"""
|
||||
|
||||
match = "message to forward not found"
|
||||
match = 'message to forward not found'
|
||||
|
||||
|
||||
class MessageToDeleteNotFound(MessageError):
|
||||
"""
|
||||
Will be raised when you try to delete very old or deleted or unknown message.
|
||||
"""
|
||||
|
||||
match = "message to delete not found"
|
||||
match = 'message to delete not found'
|
||||
|
||||
|
||||
class MessageToReplyNotFound(MessageError):
|
||||
"""
|
||||
Will be raised when you try to reply to very old or deleted or unknown message.
|
||||
"""
|
||||
|
||||
match = "message to reply not found"
|
||||
match = 'message to reply not found'
|
||||
|
||||
|
||||
class MessageIdentifierNotSpecified(MessageError):
|
||||
match = "message identifier is not specified"
|
||||
match = 'message identifier is not specified'
|
||||
|
||||
|
||||
class MessageTextIsEmpty(MessageError):
|
||||
match = "Message text is empty"
|
||||
match = 'Message text is empty'
|
||||
|
||||
|
||||
class MessageCantBeEdited(MessageError):
|
||||
match = "message can't be edited"
|
||||
match = 'message can\'t be edited'
|
||||
|
||||
|
||||
class MessageCantBeDeleted(MessageError):
|
||||
match = "message can't be deleted"
|
||||
match = 'message can\'t be deleted'
|
||||
|
||||
|
||||
class MessageToEditNotFound(MessageError):
|
||||
match = "message to edit not found"
|
||||
match = 'message to edit not found'
|
||||
|
||||
|
||||
class MessageIsTooLong(MessageError):
|
||||
match = "message is too long"
|
||||
match = 'message is too long'
|
||||
|
||||
|
||||
class ToMuchMessages(MessageError):
|
||||
"""
|
||||
Will be raised when you try to send media group with more than 10 items.
|
||||
"""
|
||||
|
||||
match = "Too much messages to send as an album"
|
||||
match = 'Too much messages to send as an album'
|
||||
|
||||
|
||||
class ObjectExpectedAsReplyMarkup(BadRequest):
|
||||
match = "object expected as reply markup"
|
||||
match = 'object expected as reply markup'
|
||||
|
||||
|
||||
class InlineKeyboardExpected(BadRequest):
|
||||
match = "inline keyboard expected"
|
||||
match = 'inline keyboard expected'
|
||||
|
||||
|
||||
class PollError(BadRequest):
|
||||
|
|
@ -243,7 +236,7 @@ class PollCantBeStopped(PollError):
|
|||
|
||||
|
||||
class PollHasAlreadyBeenClosed(PollError):
|
||||
match = "poll has already been closed"
|
||||
match = 'poll has already been closed'
|
||||
|
||||
|
||||
class PollsCantBeSentToPrivateChats(PollError):
|
||||
|
|
@ -282,112 +275,109 @@ class MessageWithPollNotFound(PollError, MessageError):
|
|||
"""
|
||||
Will be raised when you try to stop poll with message without poll
|
||||
"""
|
||||
|
||||
match = "message with poll to stop not found"
|
||||
match = 'message with poll to stop not found'
|
||||
|
||||
|
||||
class MessageIsNotAPoll(PollError, MessageError):
|
||||
"""
|
||||
Will be raised when you try to stop poll with message without poll
|
||||
"""
|
||||
|
||||
match = "message is not a poll"
|
||||
match = 'message is not a poll'
|
||||
|
||||
|
||||
class ChatNotFound(BadRequest):
|
||||
match = "chat not found"
|
||||
match = 'chat not found'
|
||||
|
||||
|
||||
class ChatIdIsEmpty(BadRequest):
|
||||
match = "chat_id is empty"
|
||||
match = 'chat_id is empty'
|
||||
|
||||
|
||||
class InvalidUserId(BadRequest):
|
||||
match = "user_id_invalid"
|
||||
text = "Invalid user id"
|
||||
match = 'user_id_invalid'
|
||||
text = 'Invalid user id'
|
||||
|
||||
|
||||
class ChatDescriptionIsNotModified(BadRequest):
|
||||
match = "chat description is not modified"
|
||||
match = 'chat description is not modified'
|
||||
|
||||
|
||||
class InvalidQueryID(BadRequest):
|
||||
match = "query is too old and response timeout expired or query id is invalid"
|
||||
match = 'query is too old and response timeout expired or query id is invalid'
|
||||
|
||||
|
||||
class InvalidPeerID(BadRequest):
|
||||
match = "PEER_ID_INVALID"
|
||||
text = "Invalid peer ID"
|
||||
match = 'PEER_ID_INVALID'
|
||||
text = 'Invalid peer ID'
|
||||
|
||||
|
||||
class InvalidHTTPUrlContent(BadRequest):
|
||||
match = "Failed to get HTTP URL content"
|
||||
match = 'Failed to get HTTP URL content'
|
||||
|
||||
|
||||
class ButtonURLInvalid(BadRequest):
|
||||
match = "BUTTON_URL_INVALID"
|
||||
text = "Button URL invalid"
|
||||
match = 'BUTTON_URL_INVALID'
|
||||
text = 'Button URL invalid'
|
||||
|
||||
|
||||
class URLHostIsEmpty(BadRequest):
|
||||
match = "URL host is empty"
|
||||
match = 'URL host is empty'
|
||||
|
||||
|
||||
class StartParamInvalid(BadRequest):
|
||||
match = "START_PARAM_INVALID"
|
||||
text = "Start param invalid"
|
||||
match = 'START_PARAM_INVALID'
|
||||
text = 'Start param invalid'
|
||||
|
||||
|
||||
class ButtonDataInvalid(BadRequest):
|
||||
match = "BUTTON_DATA_INVALID"
|
||||
text = "Button data invalid"
|
||||
match = 'BUTTON_DATA_INVALID'
|
||||
text = 'Button data invalid'
|
||||
|
||||
|
||||
class WrongFileIdentifier(BadRequest):
|
||||
match = "wrong file identifier/HTTP URL specified"
|
||||
match = 'wrong file identifier/HTTP URL specified'
|
||||
|
||||
|
||||
class GroupDeactivated(BadRequest):
|
||||
match = "group is deactivated"
|
||||
match = 'group is deactivated'
|
||||
|
||||
|
||||
class PhotoAsInputFileRequired(BadRequest):
|
||||
"""
|
||||
Will be raised when you try to set chat photo from file ID.
|
||||
"""
|
||||
|
||||
match = "Photo should be uploaded as an InputFile"
|
||||
match = 'Photo should be uploaded as an InputFile'
|
||||
|
||||
|
||||
class InvalidStickersSet(BadRequest):
|
||||
match = "STICKERSET_INVALID"
|
||||
text = "Stickers set is invalid"
|
||||
match = 'STICKERSET_INVALID'
|
||||
text = 'Stickers set is invalid'
|
||||
|
||||
|
||||
class NoStickerInRequest(BadRequest):
|
||||
match = "there is no sticker in the request"
|
||||
match = 'there is no sticker in the request'
|
||||
|
||||
|
||||
class ChatAdminRequired(BadRequest):
|
||||
match = "CHAT_ADMIN_REQUIRED"
|
||||
text = "Admin permissions is required!"
|
||||
match = 'CHAT_ADMIN_REQUIRED'
|
||||
text = 'Admin permissions is required!'
|
||||
|
||||
|
||||
class NeedAdministratorRightsInTheChannel(BadRequest):
|
||||
match = "need administrator rights in the channel chat"
|
||||
text = "Admin permissions is required!"
|
||||
match = 'need administrator rights in the channel chat'
|
||||
text = 'Admin permissions is required!'
|
||||
|
||||
|
||||
class NotEnoughRightsToPinMessage(BadRequest):
|
||||
match = "not enough rights to pin a message"
|
||||
match = 'not enough rights to pin a message'
|
||||
|
||||
|
||||
class MethodNotAvailableInPrivateChats(BadRequest):
|
||||
match = "method is available only for supergroups and channel"
|
||||
match = 'method is available only for supergroups and channel'
|
||||
|
||||
|
||||
class CantDemoteChatCreator(BadRequest):
|
||||
match = "can't demote chat creator"
|
||||
match = 'can\'t demote chat creator'
|
||||
|
||||
|
||||
class CantRestrictSelf(BadRequest):
|
||||
|
|
@ -396,34 +386,34 @@ class CantRestrictSelf(BadRequest):
|
|||
|
||||
|
||||
class NotEnoughRightsToRestrict(BadRequest):
|
||||
match = "not enough rights to restrict/unrestrict chat member"
|
||||
match = 'not enough rights to restrict/unrestrict chat member'
|
||||
|
||||
|
||||
class PhotoDimensions(BadRequest):
|
||||
match = "PHOTO_INVALID_DIMENSIONS"
|
||||
text = "Invalid photo dimensions"
|
||||
match = 'PHOTO_INVALID_DIMENSIONS'
|
||||
text = 'Invalid photo dimensions'
|
||||
|
||||
|
||||
class UnavailableMembers(BadRequest):
|
||||
match = "supergroup members are unavailable"
|
||||
match = 'supergroup members are unavailable'
|
||||
|
||||
|
||||
class TypeOfFileMismatch(BadRequest):
|
||||
match = "type of file mismatch"
|
||||
match = 'type of file mismatch'
|
||||
|
||||
|
||||
class WrongRemoteFileIdSpecified(BadRequest):
|
||||
match = "wrong remote file id specified"
|
||||
match = 'wrong remote file id specified'
|
||||
|
||||
|
||||
class PaymentProviderInvalid(BadRequest):
|
||||
match = "PAYMENT_PROVIDER_INVALID"
|
||||
text = "payment provider invalid"
|
||||
match = 'PAYMENT_PROVIDER_INVALID'
|
||||
text = 'payment provider invalid'
|
||||
|
||||
|
||||
class CurrencyTotalAmountInvalid(BadRequest):
|
||||
match = "currency_total_amount_invalid"
|
||||
text = "currency total amount invalid"
|
||||
match = 'currency_total_amount_invalid'
|
||||
text = 'currency total amount invalid'
|
||||
|
||||
|
||||
class BadWebhook(BadRequest):
|
||||
|
|
@ -431,44 +421,44 @@ class BadWebhook(BadRequest):
|
|||
|
||||
|
||||
class WebhookRequireHTTPS(BadWebhook):
|
||||
match = "HTTPS url must be provided for webhook"
|
||||
text = "bad webhook: " + match
|
||||
match = 'HTTPS url must be provided for webhook'
|
||||
text = 'bad webhook: ' + match
|
||||
|
||||
|
||||
class BadWebhookPort(BadWebhook):
|
||||
match = "Webhook can be set up only on ports 80, 88, 443 or 8443"
|
||||
text = "bad webhook: " + match
|
||||
match = 'Webhook can be set up only on ports 80, 88, 443 or 8443'
|
||||
text = 'bad webhook: ' + match
|
||||
|
||||
|
||||
class BadWebhookAddrInfo(BadWebhook):
|
||||
match = "getaddrinfo: Temporary failure in name resolution"
|
||||
text = "bad webhook: " + match
|
||||
match = 'getaddrinfo: Temporary failure in name resolution'
|
||||
text = 'bad webhook: ' + match
|
||||
|
||||
|
||||
class BadWebhookNoAddressAssociatedWithHostname(BadWebhook):
|
||||
match = "failed to resolve host: no address associated with hostname"
|
||||
match = 'failed to resolve host: no address associated with hostname'
|
||||
|
||||
|
||||
class CantParseUrl(BadRequest):
|
||||
match = "can't parse URL"
|
||||
match = 'can\'t parse URL'
|
||||
|
||||
|
||||
class UnsupportedUrlProtocol(BadRequest):
|
||||
match = "unsupported URL protocol"
|
||||
match = 'unsupported URL protocol'
|
||||
|
||||
|
||||
class CantParseEntities(BadRequest):
|
||||
match = "can't parse entities"
|
||||
match = 'can\'t parse entities'
|
||||
|
||||
|
||||
class ResultIdDuplicate(BadRequest):
|
||||
match = "result_id_duplicate"
|
||||
text = "Result ID duplicate"
|
||||
match = 'result_id_duplicate'
|
||||
text = 'Result ID duplicate'
|
||||
|
||||
|
||||
class BotDomainInvalid(BadRequest):
|
||||
match = "bot_domain_invalid"
|
||||
text = "Invalid bot domain"
|
||||
match = 'bot_domain_invalid'
|
||||
text = 'Invalid bot domain'
|
||||
|
||||
|
||||
class NotFound(TelegramAPIError, _MatchErrorMixin):
|
||||
|
|
@ -476,7 +466,7 @@ class NotFound(TelegramAPIError, _MatchErrorMixin):
|
|||
|
||||
|
||||
class MethodNotKnown(NotFound):
|
||||
match = "method not found"
|
||||
match = 'method not found'
|
||||
|
||||
|
||||
class ConflictError(TelegramAPIError, _MatchErrorMixin):
|
||||
|
|
@ -484,15 +474,13 @@ class ConflictError(TelegramAPIError, _MatchErrorMixin):
|
|||
|
||||
|
||||
class TerminatedByOtherGetUpdates(ConflictError):
|
||||
match = "terminated by other getUpdates request"
|
||||
text = (
|
||||
"Terminated by other getUpdates request; "
|
||||
"Make sure that only one bot instance is running"
|
||||
)
|
||||
match = 'terminated by other getUpdates request'
|
||||
text = 'Terminated by other getUpdates request; ' \
|
||||
'Make sure that only one bot instance is running'
|
||||
|
||||
|
||||
class CantGetUpdates(ConflictError):
|
||||
match = "can't use getUpdates method while webhook is active"
|
||||
match = 'can\'t use getUpdates method while webhook is active'
|
||||
|
||||
|
||||
class Unauthorized(TelegramAPIError, _MatchErrorMixin):
|
||||
|
|
@ -500,23 +488,23 @@ class Unauthorized(TelegramAPIError, _MatchErrorMixin):
|
|||
|
||||
|
||||
class BotKicked(Unauthorized):
|
||||
match = "Bot was kicked from a chat"
|
||||
match = 'bot was kicked from a chat'
|
||||
|
||||
|
||||
class BotBlocked(Unauthorized):
|
||||
match = "bot was blocked by the user"
|
||||
match = 'bot was blocked by the user'
|
||||
|
||||
|
||||
class UserDeactivated(Unauthorized):
|
||||
match = "user is deactivated"
|
||||
match = 'user is deactivated'
|
||||
|
||||
|
||||
class CantInitiateConversation(Unauthorized):
|
||||
match = "bot can't initiate conversation with a user"
|
||||
match = 'bot can\'t initiate conversation with a user'
|
||||
|
||||
|
||||
class CantTalkWithBots(Unauthorized):
|
||||
match = "bot can't send messages to bots"
|
||||
match = 'bot can\'t send messages to bots'
|
||||
|
||||
|
||||
class NetworkError(TelegramAPIError):
|
||||
|
|
@ -525,43 +513,34 @@ class NetworkError(TelegramAPIError):
|
|||
|
||||
class RestartingTelegram(TelegramAPIError):
|
||||
def __init__(self):
|
||||
super(RestartingTelegram, self).__init__(
|
||||
"The Telegram Bot API service is restarting. Wait few second."
|
||||
)
|
||||
super(RestartingTelegram, self).__init__('The Telegram Bot API service is restarting. Wait few second.')
|
||||
|
||||
|
||||
class RetryAfter(TelegramAPIError):
|
||||
def __init__(self, retry_after):
|
||||
super(RetryAfter, self).__init__(
|
||||
f"Flood control exceeded. Retry in {retry_after} seconds."
|
||||
)
|
||||
super(RetryAfter, self).__init__(f"Flood control exceeded. Retry in {retry_after} seconds.")
|
||||
self.timeout = retry_after
|
||||
|
||||
|
||||
class MigrateToChat(TelegramAPIError):
|
||||
def __init__(self, chat_id):
|
||||
super(MigrateToChat, self).__init__(
|
||||
f"The group has been migrated to a supergroup. New id: {chat_id}."
|
||||
)
|
||||
super(MigrateToChat, self).__init__(f"The group has been migrated to a supergroup. New id: {chat_id}.")
|
||||
self.migrate_to_chat_id = chat_id
|
||||
|
||||
|
||||
class Throttled(TelegramAPIError):
|
||||
def __init__(self, **kwargs):
|
||||
from ..dispatcher.storage import DELTA, EXCEEDED_COUNT, KEY, LAST_CALL, RATE_LIMIT, RESULT
|
||||
|
||||
self.key = kwargs.pop(KEY, "<None>")
|
||||
self.key = kwargs.pop(KEY, '<None>')
|
||||
self.called_at = kwargs.pop(LAST_CALL, time.time())
|
||||
self.rate = kwargs.pop(RATE_LIMIT, None)
|
||||
self.result = kwargs.pop(RESULT, False)
|
||||
self.exceeded_count = kwargs.pop(EXCEEDED_COUNT, 0)
|
||||
self.delta = kwargs.pop(DELTA, 0)
|
||||
self.user = kwargs.pop("user", None)
|
||||
self.chat = kwargs.pop("chat", None)
|
||||
self.user = kwargs.pop('user', None)
|
||||
self.chat = kwargs.pop('chat', None)
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"Rate limit exceeded! (Limit: {self.rate} s, "
|
||||
f"exceeded: {self.exceeded_count}, "
|
||||
return f"Rate limit exceeded! (Limit: {self.rate} s, " \
|
||||
f"exceeded: {self.exceeded_count}, " \
|
||||
f"time delta: {round(self.delta, 3)} s)"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,27 +12,18 @@ from ..bot.api import log
|
|||
from ..dispatcher.dispatcher import Dispatcher
|
||||
from ..dispatcher.webhook import BOT_DISPATCHER_KEY, DEFAULT_ROUTE_NAME, WebhookRequestHandler
|
||||
|
||||
APP_EXECUTOR_KEY = "APP_EXECUTOR"
|
||||
APP_EXECUTOR_KEY = 'APP_EXECUTOR'
|
||||
|
||||
|
||||
def _setup_callbacks(executor, on_startup=None, on_shutdown=None):
|
||||
def _setup_callbacks(executor: 'Executor', on_startup=None, on_shutdown=None):
|
||||
if on_startup is not None:
|
||||
executor.on_startup(on_startup)
|
||||
if on_shutdown is not None:
|
||||
executor.on_shutdown(on_shutdown)
|
||||
|
||||
|
||||
def start_polling(
|
||||
dispatcher,
|
||||
*,
|
||||
loop=None,
|
||||
skip_updates=False,
|
||||
reset_webhook=True,
|
||||
on_startup=None,
|
||||
on_shutdown=None,
|
||||
timeout=20,
|
||||
fast=True,
|
||||
):
|
||||
def start_polling(dispatcher, *, loop=None, skip_updates=False, reset_webhook=True,
|
||||
on_startup=None, on_shutdown=None, timeout=20, relax=0.1, fast=True):
|
||||
"""
|
||||
Start bot in long-polling mode
|
||||
|
||||
|
|
@ -47,22 +38,14 @@ def start_polling(
|
|||
executor = Executor(dispatcher, skip_updates=skip_updates, loop=loop)
|
||||
_setup_callbacks(executor, on_startup, on_shutdown)
|
||||
|
||||
executor.start_polling(reset_webhook=reset_webhook, timeout=timeout, fast=fast)
|
||||
executor.start_polling(reset_webhook=reset_webhook, timeout=timeout, relax=relax, fast=fast)
|
||||
|
||||
|
||||
def set_webhook(
|
||||
dispatcher: Dispatcher,
|
||||
webhook_path: str,
|
||||
*,
|
||||
loop: Optional[asyncio.AbstractEventLoop] = None,
|
||||
skip_updates: bool = None,
|
||||
on_startup: Optional[Callable] = None,
|
||||
on_shutdown: Optional[Callable] = None,
|
||||
check_ip: bool = False,
|
||||
retry_after: Optional[Union[str, int]] = None,
|
||||
route_name: str = DEFAULT_ROUTE_NAME,
|
||||
web_app: Optional[Application] = None,
|
||||
):
|
||||
def set_webhook(dispatcher: Dispatcher, webhook_path: str, *, loop: Optional[asyncio.AbstractEventLoop] = None,
|
||||
skip_updates: bool = None, on_startup: Optional[Callable] = None,
|
||||
on_shutdown: Optional[Callable] = None, check_ip: bool = False,
|
||||
retry_after: Optional[Union[str, int]] = None, route_name: str = DEFAULT_ROUTE_NAME,
|
||||
web_app: Optional[Application] = None):
|
||||
"""
|
||||
Set webhook for bot
|
||||
|
||||
|
|
@ -78,32 +61,17 @@ def set_webhook(
|
|||
:param web_app: Optional[Application] (default: None)
|
||||
:return:
|
||||
"""
|
||||
executor = Executor(
|
||||
dispatcher,
|
||||
skip_updates=skip_updates,
|
||||
check_ip=check_ip,
|
||||
retry_after=retry_after,
|
||||
loop=loop,
|
||||
)
|
||||
executor = Executor(dispatcher, skip_updates=skip_updates, check_ip=check_ip, retry_after=retry_after,
|
||||
loop=loop)
|
||||
_setup_callbacks(executor, on_startup, on_shutdown)
|
||||
|
||||
executor.set_webhook(webhook_path, route_name=route_name, web_app=web_app)
|
||||
return executor
|
||||
|
||||
|
||||
def start_webhook(
|
||||
dispatcher,
|
||||
webhook_path,
|
||||
*,
|
||||
loop=None,
|
||||
skip_updates=None,
|
||||
on_startup=None,
|
||||
on_shutdown=None,
|
||||
check_ip=False,
|
||||
retry_after=None,
|
||||
route_name=DEFAULT_ROUTE_NAME,
|
||||
**kwargs,
|
||||
):
|
||||
def start_webhook(dispatcher, webhook_path, *, loop=None, skip_updates=None,
|
||||
on_startup=None, on_shutdown=None, check_ip=False, retry_after=None, route_name=DEFAULT_ROUTE_NAME,
|
||||
**kwargs):
|
||||
"""
|
||||
Start bot in webhook mode
|
||||
|
||||
|
|
@ -118,21 +86,20 @@ def start_webhook(
|
|||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
executor = set_webhook(
|
||||
dispatcher=dispatcher,
|
||||
webhook_path=webhook_path,
|
||||
loop=loop,
|
||||
skip_updates=skip_updates,
|
||||
on_startup=on_startup,
|
||||
on_shutdown=on_shutdown,
|
||||
check_ip=check_ip,
|
||||
retry_after=retry_after,
|
||||
route_name=route_name,
|
||||
)
|
||||
executor = set_webhook(dispatcher=dispatcher,
|
||||
webhook_path=webhook_path,
|
||||
loop=loop,
|
||||
skip_updates=skip_updates,
|
||||
on_startup=on_startup,
|
||||
on_shutdown=on_shutdown,
|
||||
check_ip=check_ip,
|
||||
retry_after=retry_after,
|
||||
route_name=route_name)
|
||||
executor.run_app(**kwargs)
|
||||
|
||||
|
||||
def start(dispatcher, future, *, loop=None, skip_updates=None, on_startup=None, on_shutdown=None):
|
||||
def start(dispatcher, future, *, loop=None, skip_updates=None,
|
||||
on_startup=None, on_shutdown=None):
|
||||
"""
|
||||
Execute Future.
|
||||
|
||||
|
|
@ -175,7 +142,6 @@ class Executor:
|
|||
self._freeze = False
|
||||
|
||||
from aiogram import Bot, Dispatcher
|
||||
|
||||
Bot.set_current(dispatcher.bot)
|
||||
Dispatcher.set_current(dispatcher)
|
||||
|
||||
|
|
@ -194,7 +160,7 @@ class Executor:
|
|||
@property
|
||||
def web_app(self) -> web.Application:
|
||||
if self._web_app is None:
|
||||
raise RuntimeError("web.Application() is not configured!")
|
||||
raise RuntimeError('web.Application() is not configured!')
|
||||
return self._web_app
|
||||
|
||||
def on_startup(self, callback: callable, polling=True, webhook=True):
|
||||
|
|
@ -207,7 +173,7 @@ class Executor:
|
|||
"""
|
||||
self._check_frozen()
|
||||
if not webhook and not polling:
|
||||
warn("This action has no effect!", UserWarning)
|
||||
warn('This action has no effect!', UserWarning)
|
||||
return
|
||||
|
||||
if isinstance(callback, (list, tuple, set)):
|
||||
|
|
@ -230,7 +196,7 @@ class Executor:
|
|||
"""
|
||||
self._check_frozen()
|
||||
if not webhook and not polling:
|
||||
warn("This action has no effect!", UserWarning)
|
||||
warn('This action has no effect!', UserWarning)
|
||||
return
|
||||
|
||||
if isinstance(callback, (list, tuple, set)):
|
||||
|
|
@ -245,7 +211,7 @@ class Executor:
|
|||
|
||||
def _check_frozen(self):
|
||||
if self.frozen:
|
||||
raise RuntimeError("Executor is frozen!")
|
||||
raise RuntimeError('Executor is frozen!')
|
||||
|
||||
def _prepare_polling(self):
|
||||
self._check_frozen()
|
||||
|
|
@ -253,9 +219,7 @@ class Executor:
|
|||
|
||||
# self.loop.set_task_factory(context.task_factory)
|
||||
|
||||
def _prepare_webhook(
|
||||
self, path=None, handler=WebhookRequestHandler, route_name=DEFAULT_ROUTE_NAME, app=None
|
||||
):
|
||||
def _prepare_webhook(self, path=None, handler=WebhookRequestHandler, route_name=DEFAULT_ROUTE_NAME, app=None):
|
||||
self._check_frozen()
|
||||
self._freeze = True
|
||||
|
||||
|
|
@ -269,14 +233,14 @@ class Executor:
|
|||
raise RuntimeError("web.Application() is already configured!")
|
||||
|
||||
if self.retry_after:
|
||||
app["RETRY_AFTER"] = self.retry_after
|
||||
app['RETRY_AFTER'] = self.retry_after
|
||||
|
||||
if self._identity == app.get(self._identity):
|
||||
# App is already configured
|
||||
return
|
||||
|
||||
if path is not None:
|
||||
app.router.add_route("*", path, handler, name=route_name)
|
||||
app.router.add_route('*', path, handler, name=route_name)
|
||||
|
||||
async def _wrap_callback(cb, _):
|
||||
return await cb(self.dispatcher)
|
||||
|
|
@ -294,15 +258,10 @@ class Executor:
|
|||
app[APP_EXECUTOR_KEY] = self
|
||||
app[BOT_DISPATCHER_KEY] = self.dispatcher
|
||||
app[self._identity] = datetime.datetime.now()
|
||||
app["_check_ip"] = self.check_ip
|
||||
app['_check_ip'] = self.check_ip
|
||||
|
||||
def set_webhook(
|
||||
self,
|
||||
webhook_path: Optional[str] = None,
|
||||
request_handler: Any = WebhookRequestHandler,
|
||||
route_name: str = DEFAULT_ROUTE_NAME,
|
||||
web_app: Optional[Application] = None,
|
||||
):
|
||||
def set_webhook(self, webhook_path: Optional[str] = None, request_handler: Any = WebhookRequestHandler,
|
||||
route_name: str = DEFAULT_ROUTE_NAME, web_app: Optional[Application] = None):
|
||||
"""
|
||||
Set webhook for bot
|
||||
|
||||
|
|
@ -318,13 +277,8 @@ class Executor:
|
|||
def run_app(self, **kwargs):
|
||||
web.run_app(self._web_app, **kwargs)
|
||||
|
||||
def start_webhook(
|
||||
self,
|
||||
webhook_path=None,
|
||||
request_handler=WebhookRequestHandler,
|
||||
route_name=DEFAULT_ROUTE_NAME,
|
||||
**kwargs,
|
||||
):
|
||||
def start_webhook(self, webhook_path=None, request_handler=WebhookRequestHandler, route_name=DEFAULT_ROUTE_NAME,
|
||||
**kwargs):
|
||||
"""
|
||||
Start bot in webhook mode
|
||||
|
||||
|
|
@ -334,12 +288,10 @@ class Executor:
|
|||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
self.set_webhook(
|
||||
webhook_path=webhook_path, request_handler=request_handler, route_name=route_name
|
||||
)
|
||||
self.set_webhook(webhook_path=webhook_path, request_handler=request_handler, route_name=route_name)
|
||||
self.run_app(**kwargs)
|
||||
|
||||
def start_polling(self, reset_webhook=None, timeout=20, fast=True):
|
||||
def start_polling(self, reset_webhook=None, timeout=20, relax=0.1, fast=True):
|
||||
"""
|
||||
Start bot in long-polling mode
|
||||
|
||||
|
|
@ -351,11 +303,8 @@ class Executor:
|
|||
|
||||
try:
|
||||
loop.run_until_complete(self._startup_polling())
|
||||
loop.create_task(
|
||||
self.dispatcher.start_polling(
|
||||
reset_webhook=reset_webhook, timeout=timeout, fast=fast
|
||||
)
|
||||
)
|
||||
loop.create_task(self.dispatcher.start_polling(reset_webhook=reset_webhook, timeout=timeout,
|
||||
relax=relax, fast=fast))
|
||||
loop.run_forever()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
# loop.stop()
|
||||
|
|
@ -391,7 +340,7 @@ class Executor:
|
|||
async def _skip_updates(self):
|
||||
await self.dispatcher.reset_webhook(True)
|
||||
await self.dispatcher.skip_updates()
|
||||
log.warning(f"Updates are skipped successfully.")
|
||||
log.warning(f'Updates were skipped successfully.')
|
||||
|
||||
async def _welcome(self):
|
||||
user = await self.dispatcher.bot.me
|
||||
|
|
@ -412,11 +361,11 @@ class Executor:
|
|||
await callback(self.dispatcher)
|
||||
|
||||
async def _shutdown_polling(self, wait_closed=False):
|
||||
await self._shutdown()
|
||||
|
||||
for callback in self._on_shutdown_polling:
|
||||
await callback(self.dispatcher)
|
||||
|
||||
await self._shutdown()
|
||||
|
||||
if wait_closed:
|
||||
await self.dispatcher.wait_closed()
|
||||
|
||||
|
|
|
|||
|
|
@ -13,10 +13,13 @@ Example:
|
|||
>>> print(MyHelper.all())
|
||||
<<< ['barItem', 'bazItem', 'fooItem', 'lorem']
|
||||
"""
|
||||
from typing import List
|
||||
|
||||
PROPS_KEYS_ATTR_NAME = '_props_keys'
|
||||
|
||||
|
||||
class Helper:
|
||||
mode = ""
|
||||
mode = ''
|
||||
|
||||
@classmethod
|
||||
def all(cls):
|
||||
|
|
@ -37,13 +40,13 @@ class Helper:
|
|||
|
||||
|
||||
class HelperMode(Helper):
|
||||
mode = "original"
|
||||
mode = 'original'
|
||||
|
||||
SCREAMING_SNAKE_CASE = "SCREAMING_SNAKE_CASE"
|
||||
lowerCamelCase = "lowerCamelCase"
|
||||
CamelCase = "CamelCase"
|
||||
snake_case = "snake_case"
|
||||
lowercase = "lowercase"
|
||||
SCREAMING_SNAKE_CASE = 'SCREAMING_SNAKE_CASE'
|
||||
lowerCamelCase = 'lowerCamelCase'
|
||||
CamelCase = 'CamelCase'
|
||||
snake_case = 'snake_case'
|
||||
lowercase = 'lowercase'
|
||||
|
||||
@classmethod
|
||||
def all(cls):
|
||||
|
|
@ -65,10 +68,10 @@ class HelperMode(Helper):
|
|||
"""
|
||||
if text.isupper():
|
||||
return text
|
||||
result = ""
|
||||
result = ''
|
||||
for pos, symbol in enumerate(text):
|
||||
if symbol.isupper() and pos > 0:
|
||||
result += "_" + symbol
|
||||
result += '_' + symbol
|
||||
else:
|
||||
result += symbol.upper()
|
||||
return result
|
||||
|
|
@ -94,10 +97,10 @@ class HelperMode(Helper):
|
|||
:param first_upper: first symbol must be upper?
|
||||
:return:
|
||||
"""
|
||||
result = ""
|
||||
result = ''
|
||||
need_upper = False
|
||||
for pos, symbol in enumerate(text):
|
||||
if symbol == "_" and pos > 0:
|
||||
if symbol == '_' and pos > 0:
|
||||
need_upper = True
|
||||
else:
|
||||
if need_upper:
|
||||
|
|
@ -120,15 +123,15 @@ class HelperMode(Helper):
|
|||
"""
|
||||
if mode == cls.SCREAMING_SNAKE_CASE:
|
||||
return cls._screaming_snake_case(text)
|
||||
elif mode == cls.snake_case:
|
||||
if mode == cls.snake_case:
|
||||
return cls._snake_case(text)
|
||||
elif mode == cls.lowercase:
|
||||
return cls._snake_case(text).replace("_", "")
|
||||
elif mode == cls.lowerCamelCase:
|
||||
if mode == cls.lowercase:
|
||||
return cls._snake_case(text).replace('_', '')
|
||||
if mode == cls.lowerCamelCase:
|
||||
return cls._camel_case(text)
|
||||
elif mode == cls.CamelCase:
|
||||
if mode == cls.CamelCase:
|
||||
return cls._camel_case(text, True)
|
||||
elif callable(mode):
|
||||
if callable(mode):
|
||||
return mode(text)
|
||||
return text
|
||||
|
||||
|
|
@ -149,10 +152,10 @@ class Item:
|
|||
|
||||
def __set_name__(self, owner, name):
|
||||
if not name.isupper():
|
||||
raise NameError("Name for helper item must be in uppercase!")
|
||||
raise NameError('Name for helper item must be in uppercase!')
|
||||
if not self._value:
|
||||
if hasattr(owner, "mode"):
|
||||
self._value = HelperMode.apply(name, getattr(owner, "mode"))
|
||||
if hasattr(owner, 'mode'):
|
||||
self._value = HelperMode.apply(name, getattr(owner, 'mode'))
|
||||
|
||||
|
||||
class ListItem(Item):
|
||||
|
|
@ -191,3 +194,36 @@ class ItemsList(list):
|
|||
return self
|
||||
|
||||
__iadd__ = __add__ = __rand__ = __and__ = __ror__ = __or__ = add
|
||||
|
||||
|
||||
class OrderedHelperMeta(type):
|
||||
|
||||
def __new__(mcs, name, bases, namespace, **kwargs):
|
||||
cls = super().__new__(mcs, name, bases, namespace)
|
||||
|
||||
props_keys = []
|
||||
|
||||
for prop_name in (name for name, prop in namespace.items() if isinstance(prop, (Item, ListItem))):
|
||||
props_keys.append(prop_name)
|
||||
|
||||
setattr(cls, PROPS_KEYS_ATTR_NAME, props_keys)
|
||||
|
||||
return cls
|
||||
|
||||
|
||||
class OrderedHelper(metaclass=OrderedHelperMeta):
|
||||
mode = ''
|
||||
|
||||
@classmethod
|
||||
def all(cls) -> List[str]:
|
||||
"""
|
||||
Get all Items values
|
||||
"""
|
||||
result = []
|
||||
for name in getattr(cls, PROPS_KEYS_ATTR_NAME, []):
|
||||
value = getattr(cls, name)
|
||||
if isinstance(value, ItemsList):
|
||||
result.append(value[0])
|
||||
else:
|
||||
result.append(value)
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import importlib
|
||||
import os
|
||||
|
||||
JSON = "json"
|
||||
RAPIDJSON = "rapidjson"
|
||||
UJSON = "ujson"
|
||||
JSON = 'json'
|
||||
RAPIDJSON = 'rapidjson'
|
||||
UJSON = 'ujson'
|
||||
|
||||
# Detect mode
|
||||
mode = JSON
|
||||
for json_lib in (RAPIDJSON, UJSON):
|
||||
if "DISABLE_" + json_lib.upper() in os.environ:
|
||||
if 'DISABLE_' + json_lib.upper() in os.environ:
|
||||
continue
|
||||
|
||||
try:
|
||||
|
|
@ -20,35 +20,28 @@ for json_lib in (RAPIDJSON, UJSON):
|
|||
break
|
||||
|
||||
if mode == RAPIDJSON:
|
||||
|
||||
def dumps(data):
|
||||
return json.dumps(
|
||||
data,
|
||||
ensure_ascii=False,
|
||||
number_mode=json.NM_NATIVE,
|
||||
datetime_mode=json.DM_ISO8601 | json.DM_NAIVE_IS_UTC,
|
||||
)
|
||||
|
||||
def loads(data):
|
||||
return json.loads(
|
||||
data, number_mode=json.NM_NATIVE, datetime_mode=json.DM_ISO8601 | json.DM_NAIVE_IS_UTC
|
||||
)
|
||||
|
||||
|
||||
elif mode == UJSON:
|
||||
|
||||
def loads(data):
|
||||
return json.loads(data)
|
||||
|
||||
def dumps(data):
|
||||
return json.dumps(data, ensure_ascii=False)
|
||||
|
||||
|
||||
def loads(data):
|
||||
return json.loads(data, number_mode=json.NM_NATIVE)
|
||||
|
||||
elif mode == UJSON:
|
||||
def loads(data):
|
||||
return json.loads(data)
|
||||
|
||||
|
||||
def dumps(data):
|
||||
return json.dumps(data, ensure_ascii=False)
|
||||
|
||||
else:
|
||||
import json
|
||||
|
||||
|
||||
def dumps(data):
|
||||
return json.dumps(data, ensure_ascii=False)
|
||||
|
||||
|
||||
def loads(data):
|
||||
return json.loads(data)
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import contextvars
|
||||
from typing import TypeVar, Type
|
||||
|
||||
__all__ = ("DataMixin", "ContextInstanceMixin")
|
||||
__all__ = ('DataMixin', 'ContextInstanceMixin')
|
||||
|
||||
|
||||
class DataMixin:
|
||||
@property
|
||||
def data(self):
|
||||
data = getattr(self, "_data", None)
|
||||
data = getattr(self, '_data', None)
|
||||
if data is None:
|
||||
data = {}
|
||||
setattr(self, "_data", data)
|
||||
setattr(self, '_data', data)
|
||||
return data
|
||||
|
||||
def __getitem__(self, item):
|
||||
|
|
@ -26,12 +26,12 @@ class DataMixin:
|
|||
return self.data.get(key, default)
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
class ContextInstanceMixin:
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
cls.__context_instance = contextvars.ContextVar("instance_" + cls.__name__)
|
||||
cls.__context_instance = contextvars.ContextVar(f'instance_{cls.__name__}')
|
||||
return cls
|
||||
|
||||
@classmethod
|
||||
|
|
@ -43,7 +43,5 @@ class ContextInstanceMixin:
|
|||
@classmethod
|
||||
def set_current(cls: Type[T], value: T):
|
||||
if not isinstance(value, cls):
|
||||
raise TypeError(
|
||||
f"Value should be instance of '{cls.__name__}' not '{type(value).__name__}'"
|
||||
)
|
||||
raise TypeError(f'Value should be instance of {cls.__name__!r} not {type(value).__name__!r}')
|
||||
cls.__context_instance.set(value)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from babel.support import LazyProxy
|
|||
from aiogram import types
|
||||
from . import json
|
||||
|
||||
DEFAULT_FILTER = ["self", "cls"]
|
||||
DEFAULT_FILTER = ['self', 'cls']
|
||||
|
||||
|
||||
def generate_payload(exclude=None, **kwargs):
|
||||
|
|
@ -21,11 +21,10 @@ def generate_payload(exclude=None, **kwargs):
|
|||
"""
|
||||
if exclude is None:
|
||||
exclude = []
|
||||
return {
|
||||
key: value
|
||||
for key, value in kwargs.items()
|
||||
if key not in exclude + DEFAULT_FILTER and value is not None and not key.startswith("_")
|
||||
}
|
||||
return {key: value for key, value in kwargs.items() if
|
||||
key not in exclude + DEFAULT_FILTER
|
||||
and value is not None
|
||||
and not key.startswith('_')}
|
||||
|
||||
|
||||
def _normalize(obj):
|
||||
|
|
@ -39,7 +38,7 @@ def _normalize(obj):
|
|||
return [_normalize(item) for item in obj]
|
||||
elif isinstance(obj, dict):
|
||||
return {k: _normalize(v) for k, v in obj.items() if v is not None}
|
||||
elif hasattr(obj, "to_python"):
|
||||
elif hasattr(obj, 'to_python'):
|
||||
return obj.to_python()
|
||||
return obj
|
||||
|
||||
|
|
@ -53,14 +52,14 @@ def prepare_arg(value):
|
|||
"""
|
||||
if value is None:
|
||||
return value
|
||||
elif isinstance(value, (list, dict)) or hasattr(value, "to_python"):
|
||||
if isinstance(value, (list, dict)) or hasattr(value, 'to_python'):
|
||||
return json.dumps(_normalize(value))
|
||||
elif isinstance(value, datetime.timedelta):
|
||||
if isinstance(value, datetime.timedelta):
|
||||
now = datetime.datetime.now()
|
||||
return int((now + value).timestamp())
|
||||
elif isinstance(value, datetime.datetime):
|
||||
if isinstance(value, datetime.datetime):
|
||||
return round(value.timestamp())
|
||||
elif isinstance(value, LazyProxy):
|
||||
if isinstance(value, LazyProxy):
|
||||
return str(value)
|
||||
return value
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue