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:
Alex Root Junior 2019-11-03 22:19:44 +02:00
commit 87393f2475
98 changed files with 5048 additions and 4854 deletions

View file

@ -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)

View file

@ -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}

View file

@ -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

View file

@ -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)"
)

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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