Clean project

This commit is contained in:
jrootjunior 2019-11-15 12:17:57 +02:00
parent a83dd3ca63
commit bdae5fb026
259 changed files with 1303 additions and 21135 deletions

View file

@ -1,69 +0,0 @@
"""
Implementation of Telegram site authorization checking mechanism
for more information https://core.telegram.org/widgets/login#checking-authorization
Source: https://gist.github.com/JrooTJunior/887791de7273c9df5277d2b1ecadc839
"""
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
:param data:
:param token:
:return:
"""
secret = hashlib.sha256()
secret.update(token.encode("utf-8"))
sorted_params = collections.OrderedDict(sorted(data.items()))
msg = "\n".join("{}={}".format(k, v) for k, v in sorted_params.items() if k != "hash")
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
:param data:
:param token:
:return:
"""
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

@ -1,136 +0,0 @@
"""
Callback data factory
Usage:
Create instance of factory with prefix and element names:
>>> posts_query = CallbackData('post', 'post_id', 'action')
Then you can generate callback data:
>>> posts_query.new('32feff9b-92fa-48d9-9d29-621dc713743a', action='view')
<<< post:32feff9b-92fa-48d9-9d29-621dc713743a:view
Also you can generate filters:
>>> posts_query.filter(action='delete')
This filter can handle callback data by pattern: post:*:delete
"""
from __future__ import annotations
import typing
from aiogram import types
from aiogram.dispatcher.filters import Filter
class CallbackData:
"""
Callback data factory
"""
def __init__(self, prefix, *parts, sep=':'):
if not isinstance(prefix, str):
raise TypeError(f'Prefix must be instance of str not {type(prefix).__name__}')
if not prefix:
raise ValueError("Prefix can't be empty")
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
self._part_names = parts
def new(self, *args, **kwargs) -> str:
"""
Generate callback data
:param args:
:param kwargs:
:return:
"""
args = list(args)
data = [self.prefix]
for part in self._part_names:
value = kwargs.pop(part, None)
if value is None:
if args:
value = args.pop(0)
else:
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!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 were passed!')
callback_data = self.sep.join(data)
if len(callback_data) > 64:
raise ValueError('Resulted callback data is too long!')
return callback_data
def parse(self, callback_data: str) -> typing.Dict[str, str]:
"""
Parse data from the callback data
:param callback_data:
:return:
"""
prefix, *parts = callback_data.split(self.sep)
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!')
result = {'@': prefix}
result.update(zip(self._part_names, parts))
return result
def filter(self, **config) -> CallbackDataFilter:
"""
Generate filter
:param config:
:return:
"""
for key in config.keys():
if key not in self._part_names:
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
@classmethod
def validate(cls, full_config: typing.Dict[str, typing.Any]):
raise ValueError("That filter can't be used in filters factory!")
async def check(self, query: types.CallbackQuery):
try:
data = self.factory.parse(query.data)
except ValueError:
return False
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,7 +1,7 @@
import asyncio
import functools
import inspect
import warnings
import functools
from typing import Callable
@ -33,8 +33,10 @@ def deprecated(reason, stacklevel=2) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs):
warn_deprecated(msg.format(name=func.__name__, reason=reason), stacklevel=stacklevel)
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
@ -69,9 +71,9 @@ def deprecated(reason, stacklevel=2) -> Callable:
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):
@ -100,33 +102,32 @@ def renamed_argument(old_name: str, new_name: str, until_version: str, stackleve
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],
}
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],
}
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)

View file

@ -1,12 +0,0 @@
try:
import emoji
except ImportError:
raise ImportError('Need install "emoji" module.')
def emojize(text):
return emoji.emojize(text, use_aliases=True)
def demojize(text):
return emoji.demojize(text)

View file

@ -88,13 +88,13 @@ import time
# 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()
@ -104,7 +104,7 @@ class TelegramAPIError(Exception):
class _MatchErrorMixin:
match = ''
match = ""
text = None
__subclasses = []
@ -164,67 +164,72 @@ 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):
@ -236,7 +241,7 @@ class PollCantBeStopped(PollError):
class PollHasAlreadyBeenClosed(PollError):
match = 'poll has already been closed'
match = "poll has already been closed"
class PollsCantBeSentToPrivateChats(PollError):
@ -275,109 +280,112 @@ 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):
@ -386,34 +394,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):
@ -421,44 +429,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):
@ -466,7 +474,7 @@ class NotFound(TelegramAPIError, _MatchErrorMixin):
class MethodNotKnown(NotFound):
match = 'method not found'
match = "method not found"
class ConflictError(TelegramAPIError, _MatchErrorMixin):
@ -474,13 +482,15 @@ 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):
@ -488,23 +498,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):
@ -513,34 +523,43 @@ 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

@ -1,384 +0,0 @@
import asyncio
import datetime
import functools
import secrets
from typing import Callable, Union, Optional, Any
from warnings import warn
from aiohttp import web
from aiohttp.web_app import Application
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'
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, relax=0.1, fast=True):
"""
Start bot in long-polling mode
:param dispatcher:
:param loop:
:param skip_updates:
:param reset_webhook:
:param on_startup:
:param on_shutdown:
:param timeout:
"""
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, 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):
"""
Set webhook for bot
:param dispatcher: Dispatcher
:param webhook_path: str
:param loop: Optional[asyncio.AbstractEventLoop] (default: None)
:param skip_updates: bool (default: None)
:param on_startup: Optional[Callable] (default: None)
:param on_shutdown: Optional[Callable] (default: None)
:param check_ip: bool (default: False)
:param retry_after: Optional[Union[str, int]] See https://tools.ietf.org/html/rfc7231#section-7.1.3 (default: None)
:param route_name: str (default: 'webhook_handler')
: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)
_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):
"""
Start bot in webhook mode
:param dispatcher:
:param webhook_path:
:param loop:
:param skip_updates:
:param on_startup:
:param on_shutdown:
:param check_ip:
:param route_name:
: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.run_app(**kwargs)
def start(dispatcher, future, *, loop=None, skip_updates=None,
on_startup=None, on_shutdown=None):
"""
Execute Future.
:param dispatcher: instance of Dispatcher
:param future: future
:param loop: instance of AbstractEventLoop
:param skip_updates:
:param on_startup:
:param on_shutdown:
:return:
"""
executor = Executor(dispatcher, skip_updates=skip_updates, loop=loop)
_setup_callbacks(executor, on_startup, on_shutdown)
return executor.start(future)
class Executor:
"""
Main executor class
"""
def __init__(self, dispatcher, skip_updates=None, check_ip=False, retry_after=None, loop=None):
if loop is None:
loop = dispatcher.loop
self.dispatcher = dispatcher
self.skip_updates = skip_updates
self.check_ip = check_ip
self.retry_after = retry_after
self.loop = loop
self._identity = secrets.token_urlsafe(16)
self._web_app = None
self._on_startup_webhook = []
self._on_startup_polling = []
self._on_shutdown_webhook = []
self._on_shutdown_polling = []
self._freeze = False
from aiogram import Bot, Dispatcher
Bot.set_current(dispatcher.bot)
Dispatcher.set_current(dispatcher)
@property
def frozen(self):
return self._freeze
def set_web_app(self, application: web.Application):
"""
Change instance of aiohttp.web.Applicaton
:param application:
"""
self._web_app = application
@property
def web_app(self) -> web.Application:
if self._web_app is None:
raise RuntimeError('web.Application() is not configured!')
return self._web_app
def on_startup(self, callback: callable, polling=True, webhook=True):
"""
Register a callback for the startup process
:param callback:
:param polling: use with polling
:param webhook: use with webhook
"""
self._check_frozen()
if not webhook and not polling:
warn('This action has no effect!', UserWarning)
return
if isinstance(callback, (list, tuple, set)):
for cb in callback:
self.on_startup(cb, polling, webhook)
return
if polling:
self._on_startup_polling.append(callback)
if webhook:
self._on_startup_webhook.append(callback)
def on_shutdown(self, callback: callable, polling=True, webhook=True):
"""
Register a callback for the shutdown process
:param callback:
:param polling: use with polling
:param webhook: use with webhook
"""
self._check_frozen()
if not webhook and not polling:
warn('This action has no effect!', UserWarning)
return
if isinstance(callback, (list, tuple, set)):
for cb in callback:
self.on_shutdown(cb, polling, webhook)
return
if polling:
self._on_shutdown_polling.append(callback)
if webhook:
self._on_shutdown_webhook.append(callback)
def _check_frozen(self):
if self.frozen:
raise RuntimeError('Executor is frozen!')
def _prepare_polling(self):
self._check_frozen()
self._freeze = True
# self.loop.set_task_factory(context.task_factory)
def _prepare_webhook(self, path=None, handler=WebhookRequestHandler, route_name=DEFAULT_ROUTE_NAME, app=None):
self._check_frozen()
self._freeze = True
# self.loop.set_task_factory(context.task_factory)
if app is not None:
self._web_app = app
elif self._web_app is None:
self._web_app = app = web.Application()
else:
raise RuntimeError("web.Application() is already configured!")
if 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)
async def _wrap_callback(cb, _):
return await cb(self.dispatcher)
for callback in self._on_startup_webhook:
app.on_startup.append(functools.partial(_wrap_callback, callback))
# for callback in self._on_shutdown_webhook:
# app.on_shutdown.append(functools.partial(_wrap_callback, callback))
async def _on_shutdown(_):
await self._shutdown_webhook()
app.on_shutdown.append(_on_shutdown)
app[APP_EXECUTOR_KEY] = self
app[BOT_DISPATCHER_KEY] = self.dispatcher
app[self._identity] = datetime.datetime.now()
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):
"""
Set webhook for bot
:param webhook_path: Optional[str] (default: None)
:param request_handler: Any (default: WebhookRequestHandler)
:param route_name: str Name of webhook handler route (default: 'webhook_handler')
:param web_app: Optional[Application] (default: None)
:return:
"""
self._prepare_webhook(webhook_path, request_handler, route_name, web_app)
self.loop.run_until_complete(self._startup_webhook())
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):
"""
Start bot in webhook mode
:param webhook_path:
:param request_handler:
:param route_name: Name of webhook handler route
:param kwargs:
:return:
"""
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, relax=0.1, fast=True):
"""
Start bot in long-polling mode
:param reset_webhook:
:param timeout:
"""
self._prepare_polling()
loop: asyncio.AbstractEventLoop = self.loop
try:
loop.run_until_complete(self._startup_polling())
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()
pass
finally:
loop.run_until_complete(self._shutdown_polling())
log.warning("Goodbye!")
def start(self, future):
"""
Execute Future.
Return the Future's result, or raise its exception.
:param future:
:return:
"""
self._check_frozen()
self._freeze = True
loop: asyncio.AbstractEventLoop = self.loop
try:
loop.run_until_complete(self._startup_polling())
result = loop.run_until_complete(future)
except (KeyboardInterrupt, SystemExit):
result = None
loop.stop()
finally:
loop.run_until_complete(self._shutdown_polling())
log.warning("Goodbye!")
return result
async def _skip_updates(self):
await self.dispatcher.reset_webhook(True)
await self.dispatcher.skip_updates()
log.warning(f'Updates were skipped successfully.')
async def _welcome(self):
user = await self.dispatcher.bot.me
log.info(f"Bot: {user.full_name} [@{user.username}]")
async def _shutdown(self):
self.dispatcher.stop_polling()
await self.dispatcher.storage.close()
await self.dispatcher.storage.wait_closed()
await self.dispatcher.bot.close()
async def _startup_polling(self):
await self._welcome()
if self.skip_updates:
await self._skip_updates()
for callback in self._on_startup_polling:
await callback(self.dispatcher)
async def _shutdown_polling(self, wait_closed=False):
for callback in self._on_shutdown_polling:
await callback(self.dispatcher)
await self._shutdown()
if wait_closed:
await self.dispatcher.wait_closed()
async def _shutdown_webhook(self, wait_closed=False):
for callback in self._on_shutdown_webhook:
await callback(self.dispatcher)
await self._shutdown()
if wait_closed:
await self.dispatcher.wait_closed()
async def _startup_webhook(self):
await self._welcome()
if self.skip_updates:
await self._skip_updates()

View file

@ -15,11 +15,11 @@ Example:
"""
from typing import List
PROPS_KEYS_ATTR_NAME = '_props_keys'
PROPS_KEYS_ATTR_NAME = "_props_keys"
class Helper:
mode = ''
mode = ""
@classmethod
def all(cls):
@ -40,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):
@ -68,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
@ -97,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:
@ -126,7 +126,7 @@ class HelperMode(Helper):
if mode == cls.snake_case:
return cls._snake_case(text)
if mode == cls.lowercase:
return cls._snake_case(text).replace('_', '')
return cls._snake_case(text).replace("_", "")
if mode == cls.lowerCamelCase:
return cls._camel_case(text)
if mode == cls.CamelCase:
@ -152,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):
@ -197,13 +197,14 @@ class ItemsList(list):
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))):
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)
@ -212,7 +213,7 @@ class OrderedHelperMeta(type):
class OrderedHelper(metaclass=OrderedHelperMeta):
mode = ''
mode = ""
@classmethod
def all(cls) -> List[str]:

View file

@ -1,47 +0,0 @@
import importlib
import os
JSON = 'json'
RAPIDJSON = 'rapidjson'
UJSON = 'ujson'
# Detect mode
mode = JSON
for json_lib in (RAPIDJSON, UJSON):
if 'DISABLE_' + json_lib.upper() in os.environ:
continue
try:
json = importlib.import_module(json_lib)
except ImportError:
continue
else:
mode = json_lib
break
if mode == RAPIDJSON:
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
from typing import Type, TypeVar
__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(f'instance_{cls.__name__}')
cls.__context_instance = contextvars.ContextVar(f"instance_{cls.__name__}")
return cls
@classmethod
@ -43,5 +43,7 @@ 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__!r} not {type(value).__name__!r}')
raise TypeError(
f"Value should be instance of {cls.__name__!r} not {type(value).__name__!r}"
)
cls.__context_instance.set(value)

View file

@ -1,59 +0,0 @@
import typing
MAX_MESSAGE_LENGTH = 4096
def split_text(text: str, length: int = MAX_MESSAGE_LENGTH) -> typing.List[str]:
"""
Split long text
:param text:
:param length:
:return: list of parts
:rtype: :obj:`typing.List[str]`
"""
return [text[i : i + length] for i in range(0, len(text), length)]
def safe_split_text(text: str, length: int = MAX_MESSAGE_LENGTH) -> typing.List[str]:
"""
Split long text
:param text:
:param length:
:return:
"""
# TODO: More informative description
temp_text = text
parts = []
while temp_text:
if len(temp_text) > length:
try:
split_pos = temp_text[:length].rindex(" ")
except ValueError:
split_pos = length
if split_pos < length // 4 * 3:
split_pos = length
parts.append(temp_text[:split_pos])
temp_text = temp_text[split_pos:].lstrip()
else:
parts.append(temp_text)
break
return parts
def paginate(data: typing.Iterable, page: int = 0, limit: int = 10) -> typing.Iterable:
"""
Slice data over pages
:param data: any iterable object
:type data: :obj:`typing.Iterable`
:param page: number of page
:type page: :obj:`int`
:param limit: items per page
:type limit: :obj:`int`
:return: sliced object
:rtype: :obj:`typing.Iterable`
"""
return data[page * limit : page * limit + limit]

View file

@ -1,83 +0,0 @@
import datetime
import secrets
from babel.support import LazyProxy
from aiogram import types
from . import json
DEFAULT_FILTER = ['self', 'cls']
def generate_payload(exclude=None, **kwargs):
"""
Generate payload
Usage: payload = generate_payload(**locals(), exclude=['foo'])
:param exclude:
:param kwargs:
:return: dict
"""
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('_')}
def _normalize(obj):
"""
Normalize dicts and lists
:param obj:
:return: normalized object
"""
if isinstance(obj, list):
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'):
return obj.to_python()
return obj
def prepare_arg(value):
"""
Stringify dicts/lists and convert datetime/timedelta to unix-time
:param value:
:return:
"""
if value is None:
return value
if isinstance(value, (list, dict)) or hasattr(value, 'to_python'):
return json.dumps(_normalize(value))
if isinstance(value, datetime.timedelta):
now = datetime.datetime.now()
return int((now + value).timestamp())
if isinstance(value, datetime.datetime):
return round(value.timestamp())
if isinstance(value, LazyProxy):
return str(value)
return value
def prepare_file(payload, files, key, file):
if isinstance(file, str):
payload[key] = file
elif file is not None:
files[key] = file
def prepare_attachment(payload, files, key, file):
if isinstance(file, str):
payload[key] = file
elif isinstance(file, types.InputFile):
payload[key] = file.attach
files[file.attachment_key] = file.file
elif file is not None:
file_attach_name = secrets.token_urlsafe(16)
payload[key] = "attach://" + file_attach_name
files[file_attach_name] = file