mirror of
https://github.com/aiogram/aiogram.git
synced 2026-04-08 16:37:47 +00:00
Merge branch 'dev-3.x' into dev-3.x
This commit is contained in:
commit
4b4c9055ff
37 changed files with 1112 additions and 839 deletions
1
.github/workflows/tests.yml
vendored
1
.github/workflows/tests.yml
vendored
|
|
@ -19,7 +19,6 @@ jobs:
|
|||
- macos-latest
|
||||
- windows-latest
|
||||
python-version:
|
||||
- 3.7
|
||||
- 3.8
|
||||
- 3.9
|
||||
|
||||
|
|
|
|||
|
|
@ -9,4 +9,4 @@
|
|||
[](https://pypi.python.org/pypi/aiogram)
|
||||
[](https://app.codecov.io/gh/aiogram/aiogram)
|
||||
|
||||
**aiogram** modern and fully asynchronous framework for [Telegram Bot API](https://core.telegram.org/bots/api) written in Python 3.7 with [asyncio](https://docs.python.org/3/library/asyncio.html) and [aiohttp](https://github.com/aio-libs/aiohttp). It helps you to make your bots faster and simpler.
|
||||
**aiogram** modern and fully asynchronous framework for [Telegram Bot API](https://core.telegram.org/bots/api) written in Python 3.8 with [asyncio](https://docs.python.org/3/library/asyncio.html) and [aiohttp](https://github.com/aio-libs/aiohttp). It helps you to make your bots faster and simpler.
|
||||
|
|
|
|||
|
|
@ -302,7 +302,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
:param method:
|
||||
:return:
|
||||
"""
|
||||
return await self.session.make_request(self, method, timeout=request_timeout)
|
||||
return await self.session(self, method, timeout=request_timeout)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""
|
||||
|
|
|
|||
64
aiogram/client/errors_middleware.py
Normal file
64
aiogram/client/errors_middleware.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING, List, Type
|
||||
|
||||
from aiogram.methods import Response, TelegramMethod
|
||||
from aiogram.types import TelegramObject
|
||||
from aiogram.utils.exceptions.base import TelegramAPIError
|
||||
from aiogram.utils.exceptions.exceptions import (
|
||||
CantParseEntitiesStartTag,
|
||||
CantParseEntitiesUnclosed,
|
||||
CantParseEntitiesUnmatchedTags,
|
||||
CantParseEntitiesUnsupportedTag,
|
||||
DetailedTelegramAPIError,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aiogram.client.bot import Bot
|
||||
from aiogram.client.session.base import NextRequestMiddlewareType
|
||||
|
||||
|
||||
class RequestErrorMiddleware:
|
||||
def __init__(self) -> None:
|
||||
self._registry: List[Type[DetailedTelegramAPIError]] = [
|
||||
CantParseEntitiesStartTag,
|
||||
CantParseEntitiesUnmatchedTags,
|
||||
CantParseEntitiesUnclosed,
|
||||
CantParseEntitiesUnsupportedTag,
|
||||
]
|
||||
|
||||
def mount(self, error: Type[DetailedTelegramAPIError]) -> Type[DetailedTelegramAPIError]:
|
||||
if error in self:
|
||||
raise ValueError(f"{error!r} is already registered")
|
||||
if not hasattr(error, "patterns"):
|
||||
raise ValueError(f"{error!r} has no attribute 'patterns'")
|
||||
self._registry.append(error)
|
||||
return error
|
||||
|
||||
def detect_error(self, err: TelegramAPIError) -> TelegramAPIError:
|
||||
message = err.message
|
||||
for variant in self._registry:
|
||||
for pattern in variant.patterns:
|
||||
if match := re.match(pattern, message):
|
||||
return variant(
|
||||
method=err.method,
|
||||
message=err.message,
|
||||
match=match,
|
||||
)
|
||||
return err
|
||||
|
||||
def __contains__(self, item: Type[DetailedTelegramAPIError]) -> bool:
|
||||
return item in self._registry
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
bot: Bot,
|
||||
method: TelegramMethod[TelegramObject],
|
||||
make_request: NextRequestMiddlewareType,
|
||||
) -> Response[TelegramObject]:
|
||||
try:
|
||||
return await make_request(bot, method)
|
||||
except TelegramAPIError as e:
|
||||
detected_err = self.detect_error(err=e)
|
||||
raise detected_err from e
|
||||
|
|
@ -10,7 +10,6 @@ from typing import (
|
|||
Optional,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
|
@ -19,12 +18,12 @@ from aiohttp import BasicAuth, ClientSession, FormData, TCPConnector
|
|||
|
||||
from aiogram.methods import Request, TelegramMethod
|
||||
|
||||
from ...methods.base import TelegramType
|
||||
from .base import UNSET, BaseSession
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from ..bot import Bot
|
||||
|
||||
T = TypeVar("T")
|
||||
_ProxyBasic = Union[str, Tuple[str, BasicAuth]]
|
||||
_ProxyChain = Iterable[_ProxyBasic]
|
||||
_ProxyType = Union[_ProxyChain, _ProxyBasic]
|
||||
|
|
@ -76,6 +75,8 @@ def _prepare_connector(chain_or_plain: _ProxyType) -> Tuple[Type["TCPConnector"]
|
|||
|
||||
class AiohttpSession(BaseSession):
|
||||
def __init__(self, proxy: Optional[_ProxyType] = None):
|
||||
super().__init__()
|
||||
|
||||
self._session: Optional[ClientSession] = None
|
||||
self._connector_type: Type[TCPConnector] = TCPConnector
|
||||
self._connector_init: Dict[str, Any] = {}
|
||||
|
|
@ -86,7 +87,7 @@ class AiohttpSession(BaseSession):
|
|||
try:
|
||||
self._setup_proxy_connector(proxy)
|
||||
except ImportError as exc: # pragma: no cover
|
||||
raise UserWarning(
|
||||
raise RuntimeError(
|
||||
"In order to use aiohttp client for proxy requests, install "
|
||||
"https://pypi.org/project/aiohttp-socks/"
|
||||
) from exc
|
||||
|
|
@ -130,8 +131,8 @@ class AiohttpSession(BaseSession):
|
|||
return form
|
||||
|
||||
async def make_request(
|
||||
self, bot: Bot, call: TelegramMethod[T], timeout: Optional[int] = None
|
||||
) -> T:
|
||||
self, bot: Bot, call: TelegramMethod[TelegramType], timeout: Optional[int] = None
|
||||
) -> TelegramType:
|
||||
session = await self.create_session()
|
||||
|
||||
request = call.build_request(bot)
|
||||
|
|
@ -141,11 +142,10 @@ class AiohttpSession(BaseSession):
|
|||
async with session.post(
|
||||
url, data=form, timeout=self.timeout if timeout is None else timeout
|
||||
) as resp:
|
||||
raw_result = await resp.json(loads=self.json_loads)
|
||||
raw_result = await resp.text()
|
||||
|
||||
response = call.build_response(raw_result)
|
||||
self.raise_for_status(response)
|
||||
return cast(T, response.result)
|
||||
response = self.check_response(method=call, status_code=resp.status, content=raw_result)
|
||||
return cast(TelegramType, response.result)
|
||||
|
||||
async def stream_content(
|
||||
self, url: str, timeout: int, chunk_size: int
|
||||
|
|
|
|||
|
|
@ -3,32 +3,44 @@ from __future__ import annotations
|
|||
import abc
|
||||
import datetime
|
||||
import json
|
||||
from functools import partial
|
||||
from types import TracebackType
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
AsyncGenerator,
|
||||
Awaitable,
|
||||
Callable,
|
||||
ClassVar,
|
||||
List,
|
||||
Optional,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
from aiogram.utils.exceptions import TelegramAPIError
|
||||
from aiogram.utils.exceptions.base import TelegramAPIError
|
||||
from aiogram.utils.helper import Default
|
||||
|
||||
from ...methods import Response, TelegramMethod
|
||||
from ...types import UNSET
|
||||
from ...methods.base import TelegramType
|
||||
from ...types import UNSET, TelegramObject
|
||||
from ...utils.exceptions.special import MigrateToChat, RetryAfter
|
||||
from ..errors_middleware import RequestErrorMiddleware
|
||||
from ..telegram import PRODUCTION, TelegramAPIServer
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from ..bot import Bot
|
||||
|
||||
T = TypeVar("T")
|
||||
_JsonLoads = Callable[..., Any]
|
||||
_JsonDumps = Callable[..., str]
|
||||
NextRequestMiddlewareType = Callable[
|
||||
["Bot", TelegramMethod[TelegramObject]], Awaitable[Response[TelegramObject]]
|
||||
]
|
||||
RequestMiddlewareType = Callable[
|
||||
["Bot", TelegramMethod[TelegramType], NextRequestMiddlewareType],
|
||||
Awaitable[Response[TelegramType]],
|
||||
]
|
||||
|
||||
|
||||
class BaseSession(abc.ABC):
|
||||
|
|
@ -43,16 +55,40 @@ class BaseSession(abc.ABC):
|
|||
timeout: Default[float] = Default(fget=lambda self: float(self.__class__.default_timeout))
|
||||
"""Session scope request timeout"""
|
||||
|
||||
@classmethod
|
||||
def raise_for_status(cls, response: Response[T]) -> None:
|
||||
errors_middleware: ClassVar[RequestErrorMiddleware] = RequestErrorMiddleware()
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.middlewares: List[RequestMiddlewareType[TelegramObject]] = [
|
||||
self.errors_middleware,
|
||||
]
|
||||
|
||||
def check_response(
|
||||
self, method: TelegramMethod[TelegramType], status_code: int, content: str
|
||||
) -> Response[TelegramType]:
|
||||
"""
|
||||
Check response status
|
||||
|
||||
:param response: Response instance
|
||||
"""
|
||||
json_data = self.json_loads(content)
|
||||
response = method.build_response(json_data)
|
||||
if response.ok:
|
||||
return
|
||||
raise TelegramAPIError(response.description)
|
||||
return response
|
||||
|
||||
description = cast(str, response.description)
|
||||
if parameters := response.parameters:
|
||||
if parameters.retry_after:
|
||||
raise RetryAfter(
|
||||
method=method, message=description, retry_after=parameters.retry_after
|
||||
)
|
||||
if parameters.migrate_to_chat_id:
|
||||
raise MigrateToChat(
|
||||
method=method,
|
||||
message=description,
|
||||
migrate_to_chat_id=parameters.migrate_to_chat_id,
|
||||
)
|
||||
raise TelegramAPIError(
|
||||
method=method,
|
||||
message=description,
|
||||
)
|
||||
|
||||
@abc.abstractmethod
|
||||
async def close(self) -> None: # pragma: no cover
|
||||
|
|
@ -63,8 +99,8 @@ class BaseSession(abc.ABC):
|
|||
|
||||
@abc.abstractmethod
|
||||
async def make_request(
|
||||
self, bot: Bot, method: TelegramMethod[T], timeout: Optional[int] = UNSET
|
||||
) -> T: # pragma: no cover
|
||||
self, bot: Bot, method: TelegramMethod[TelegramType], timeout: Optional[int] = UNSET
|
||||
) -> TelegramType: # pragma: no cover
|
||||
"""
|
||||
Make request to Telegram Bot API
|
||||
|
||||
|
|
@ -111,6 +147,20 @@ class BaseSession(abc.ABC):
|
|||
return {k: self.clean_json(v) for k, v in value.items() if v is not None}
|
||||
return value
|
||||
|
||||
def middleware(
|
||||
self, middleware: RequestMiddlewareType[TelegramObject]
|
||||
) -> RequestMiddlewareType[TelegramObject]:
|
||||
self.middlewares.append(middleware)
|
||||
return middleware
|
||||
|
||||
async def __call__(
|
||||
self, bot: Bot, method: TelegramMethod[TelegramType], timeout: Optional[int] = UNSET
|
||||
) -> TelegramType:
|
||||
middleware = partial(self.make_request, timeout=timeout)
|
||||
for m in reversed(self.middlewares):
|
||||
middleware = partial(m, make_request=middleware) # type: ignore
|
||||
return await middleware(bot, method)
|
||||
|
||||
async def __aenter__(self) -> BaseSession:
|
||||
return self
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ from typing import Any, AsyncGenerator, Dict, Optional, Union, cast
|
|||
|
||||
from .. import loggers
|
||||
from ..client.bot import Bot
|
||||
from ..methods import TelegramMethod
|
||||
from ..methods import GetUpdates, TelegramMethod
|
||||
from ..types import TelegramObject, Update, User
|
||||
from ..utils.exceptions import TelegramAPIError
|
||||
from ..utils.exceptions.base import TelegramAPIError
|
||||
from .event.bases import UNHANDLED, SkipHandler
|
||||
from .event.telegram import TelegramEventObserver
|
||||
from .fsm.context import FSMContext
|
||||
|
|
@ -119,16 +119,22 @@ class Dispatcher(Router):
|
|||
return await self.feed_update(bot=bot, update=parsed_update, **kwargs)
|
||||
|
||||
@classmethod
|
||||
async def _listen_updates(cls, bot: Bot) -> AsyncGenerator[Update, None]:
|
||||
async def _listen_updates(
|
||||
cls, bot: Bot, polling_timeout: int = 30
|
||||
) -> AsyncGenerator[Update, None]:
|
||||
"""
|
||||
Infinity updates reader
|
||||
"""
|
||||
update_id: Optional[int] = None
|
||||
get_updates = GetUpdates(timeout=polling_timeout)
|
||||
kwargs = {}
|
||||
if bot.session.timeout:
|
||||
kwargs["request_timeout"] = int(bot.session.timeout + polling_timeout)
|
||||
while True:
|
||||
# TODO: Skip restarting telegram error
|
||||
for update in await bot.get_updates(offset=update_id):
|
||||
updates = await bot(get_updates, **kwargs)
|
||||
for update in updates:
|
||||
yield update
|
||||
update_id = update.update_id + 1
|
||||
get_updates.offset = update.update_id + 1
|
||||
|
||||
async def _listen_update(self, update: Update, **kwargs: Any) -> Any:
|
||||
"""
|
||||
|
|
@ -249,7 +255,7 @@ class Dispatcher(Router):
|
|||
)
|
||||
return True # because update was processed but unsuccessful
|
||||
|
||||
async def _polling(self, bot: Bot, **kwargs: Any) -> None:
|
||||
async def _polling(self, bot: Bot, polling_timeout: int = 30, **kwargs: Any) -> None:
|
||||
"""
|
||||
Internal polling process
|
||||
|
||||
|
|
@ -257,7 +263,7 @@ class Dispatcher(Router):
|
|||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
async for update in self._listen_updates(bot):
|
||||
async for update in self._listen_updates(bot, polling_timeout=polling_timeout):
|
||||
await self._process_update(bot=bot, update=update, **kwargs)
|
||||
|
||||
async def _feed_webhook_update(self, bot: Bot, update: Update, **kwargs: Any) -> Any:
|
||||
|
|
@ -336,7 +342,7 @@ class Dispatcher(Router):
|
|||
|
||||
return None
|
||||
|
||||
async def start_polling(self, *bots: Bot, **kwargs: Any) -> None:
|
||||
async def start_polling(self, *bots: Bot, polling_timeout: int = 10, **kwargs: Any) -> None:
|
||||
"""
|
||||
Polling runner
|
||||
|
||||
|
|
@ -356,7 +362,9 @@ class Dispatcher(Router):
|
|||
loggers.dispatcher.info(
|
||||
"Run polling for bot @%s id=%d - %r", user.username, bot.id, user.full_name
|
||||
)
|
||||
coro_list.append(self._polling(bot=bot, **kwargs))
|
||||
coro_list.append(
|
||||
self._polling(bot=bot, polling_timeout=polling_timeout, **kwargs)
|
||||
)
|
||||
await asyncio.gather(*coro_list)
|
||||
finally:
|
||||
for bot in bots: # Close sessions
|
||||
|
|
@ -364,16 +372,19 @@ class Dispatcher(Router):
|
|||
loggers.dispatcher.info("Polling stopped")
|
||||
await self.emit_shutdown(**workflow_data)
|
||||
|
||||
def run_polling(self, *bots: Bot, **kwargs: Any) -> None:
|
||||
def run_polling(self, *bots: Bot, polling_timeout: int = 30, **kwargs: Any) -> None:
|
||||
"""
|
||||
Run many bots with polling
|
||||
|
||||
:param bots:
|
||||
:param kwargs:
|
||||
:param bots: Bot instances
|
||||
:param polling_timeout: Poling timeout
|
||||
:param kwargs: contextual data
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
return asyncio.run(self.start_polling(*bots, **kwargs))
|
||||
return asyncio.run(
|
||||
self.start_polling(*bots, **kwargs, polling_timeout=polling_timeout)
|
||||
)
|
||||
except (KeyboardInterrupt, SystemExit): # pragma: no cover
|
||||
# Allow to graceful shutdown
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ class TelegramEventObserver:
|
|||
return UNHANDLED
|
||||
|
||||
def __call__(
|
||||
self, *args: FilterType, **bound_filters: BaseFilter
|
||||
self, *args: FilterType, **bound_filters: Any
|
||||
) -> Callable[[CallbackType], CallbackType]:
|
||||
"""
|
||||
Decorator for registering event handlers
|
||||
|
|
|
|||
113
aiogram/dispatcher/filters/callback_data.py
Normal file
113
aiogram/dispatcher/filters/callback_data.py
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from fractions import Fraction
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Type, TypeVar, Union
|
||||
from uuid import UUID
|
||||
|
||||
from magic_filter import MagicFilter
|
||||
from pydantic import BaseModel
|
||||
|
||||
from aiogram.dispatcher.filters import BaseFilter
|
||||
from aiogram.types import CallbackQuery
|
||||
|
||||
T = TypeVar("T", bound="CallbackData")
|
||||
|
||||
MAX_CALLBACK_LENGTH: int = 64
|
||||
|
||||
|
||||
class CallbackDataException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class CallbackData(BaseModel):
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
sep: str
|
||||
prefix: str
|
||||
|
||||
def __init_subclass__(cls, **kwargs: Any) -> None:
|
||||
if "prefix" not in kwargs:
|
||||
raise ValueError(
|
||||
f"prefix required, usage example: "
|
||||
f"`class {cls.__name__}(CallbackData, prefix='my_callback'): ...`"
|
||||
)
|
||||
cls.sep = kwargs.pop("sep", ":")
|
||||
cls.prefix = kwargs.pop("prefix")
|
||||
if cls.sep in cls.prefix:
|
||||
raise ValueError(
|
||||
f"Separator symbol {cls.sep!r} can not be used inside prefix {cls.prefix!r}"
|
||||
)
|
||||
|
||||
def _encode_value(self, key: str, value: Any) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
if isinstance(value, Enum):
|
||||
return str(value.value)
|
||||
if isinstance(value, (int, str, float, Decimal, Fraction, UUID)):
|
||||
return str(value)
|
||||
raise ValueError(
|
||||
f"Attribute {key}={value!r} of type {type(value).__name__!r}"
|
||||
f" can not be packed to callback data"
|
||||
)
|
||||
|
||||
def pack(self) -> str:
|
||||
result = [self.prefix]
|
||||
for key, value in self.dict().items():
|
||||
encoded = self._encode_value(key, value)
|
||||
if self.sep in encoded:
|
||||
raise ValueError(
|
||||
f"Separator symbol {self.sep!r} can not be used in value {key}={encoded!r}"
|
||||
)
|
||||
result.append(encoded)
|
||||
callback_data = self.sep.join(result)
|
||||
if len(callback_data.encode()) > MAX_CALLBACK_LENGTH:
|
||||
raise ValueError(
|
||||
f"Resulted callback data is too long! len({callback_data!r}.encode()) > {MAX_CALLBACK_LENGTH}"
|
||||
)
|
||||
return callback_data
|
||||
|
||||
@classmethod
|
||||
def unpack(cls: Type[T], value: str) -> T:
|
||||
prefix, *parts = value.split(cls.sep)
|
||||
names = cls.__fields__.keys()
|
||||
if len(parts) != len(names):
|
||||
raise TypeError(
|
||||
f"Callback data {cls.__name__!r} takes {len(names)} arguments but {len(parts)} were given"
|
||||
)
|
||||
if prefix != cls.prefix:
|
||||
raise ValueError(f"Bad prefix ({prefix!r} != {cls.prefix!r})")
|
||||
payload = {}
|
||||
for k, v in zip(names, parts): # type: str, Optional[str]
|
||||
if field := cls.__fields__.get(k):
|
||||
if v == "" and not field.required:
|
||||
v = None
|
||||
payload[k] = v
|
||||
return cls(**payload)
|
||||
|
||||
@classmethod
|
||||
def filter(cls, rule: MagicFilter) -> CallbackQueryFilter:
|
||||
return CallbackQueryFilter(callback_data=cls, rule=rule)
|
||||
|
||||
class Config:
|
||||
use_enum_values = True
|
||||
|
||||
|
||||
class CallbackQueryFilter(BaseFilter):
|
||||
callback_data: Type[CallbackData]
|
||||
rule: MagicFilter
|
||||
|
||||
async def __call__(self, query: CallbackQuery) -> Union[bool, Dict[str, Any]]:
|
||||
if not isinstance(query, CallbackQuery) or not query.data:
|
||||
return False
|
||||
try:
|
||||
callback_data = self.callback_data.unpack(query.data)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
if self.rule.resolve(callback_data):
|
||||
return {"callback_data": callback_data}
|
||||
return False
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
|
@ -28,9 +28,9 @@ class FSMContextMiddleware(BaseMiddleware[Update]):
|
|||
data["fsm_storage"] = self.storage
|
||||
if context:
|
||||
data.update({"state": context, "raw_state": await context.get_state()})
|
||||
if self.isolate_events:
|
||||
async with self.storage.lock():
|
||||
return await handler(event, data)
|
||||
if self.isolate_events:
|
||||
async with self.storage.lock(chat_id=context.chat_id, user_id=context.user_id):
|
||||
return await handler(event, data)
|
||||
return await handler(event, data)
|
||||
|
||||
def resolve_event_context(self, data: Dict[str, Any]) -> Optional[FSMContext]:
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ StateType = Optional[Union[str, State]]
|
|||
class BaseStorage(ABC):
|
||||
@abstractmethod
|
||||
@asynccontextmanager
|
||||
async def lock(self) -> AsyncGenerator[None, None]: # pragma: no cover
|
||||
async def lock(
|
||||
self, chat_id: int, user_id: int
|
||||
) -> AsyncGenerator[None, None]: # pragma: no cover
|
||||
yield None
|
||||
|
||||
@abstractmethod
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from aiogram.dispatcher.fsm.storage.base import BaseStorage, StateType
|
|||
class MemoryStorageRecord:
|
||||
data: Dict[str, Any] = field(default_factory=dict)
|
||||
state: Optional[str] = None
|
||||
lock: Lock = field(default_factory=Lock)
|
||||
|
||||
|
||||
class MemoryStorage(BaseStorage):
|
||||
|
|
@ -19,11 +20,10 @@ class MemoryStorage(BaseStorage):
|
|||
self.storage: DefaultDict[int, DefaultDict[int, MemoryStorageRecord]] = defaultdict(
|
||||
lambda: defaultdict(MemoryStorageRecord)
|
||||
)
|
||||
self._lock = Lock()
|
||||
|
||||
@asynccontextmanager
|
||||
async def lock(self) -> AsyncGenerator[None, None]:
|
||||
async with self._lock:
|
||||
async def lock(self, chat_id: int, user_id: int) -> AsyncGenerator[None, None]:
|
||||
async with self.storage[chat_id][user_id].lock:
|
||||
yield None
|
||||
|
||||
async def set_state(self, chat_id: int, user_id: int, state: StateType = None) -> None:
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from ..types import UNSET, InputFile, ResponseParameters
|
|||
if TYPE_CHECKING: # pragma: no cover
|
||||
from ..client.bot import Bot
|
||||
|
||||
T = TypeVar("T")
|
||||
TelegramType = TypeVar("TelegramType", bound=Any)
|
||||
|
||||
|
||||
class Request(BaseModel):
|
||||
|
|
@ -31,14 +31,15 @@ class Request(BaseModel):
|
|||
}
|
||||
|
||||
|
||||
class Response(ResponseParameters, GenericModel, Generic[T]):
|
||||
class Response(GenericModel, Generic[TelegramType]):
|
||||
ok: bool
|
||||
result: Optional[T] = None
|
||||
result: Optional[TelegramType] = None
|
||||
description: Optional[str] = None
|
||||
error_code: Optional[int] = None
|
||||
parameters: Optional[ResponseParameters] = None
|
||||
|
||||
|
||||
class TelegramMethod(abc.ABC, BaseModel, Generic[T]):
|
||||
class TelegramMethod(abc.ABC, BaseModel, Generic[TelegramType]):
|
||||
class Config(BaseConfig):
|
||||
# use_enum_values = True
|
||||
extra = Extra.allow
|
||||
|
|
@ -76,14 +77,14 @@ class TelegramMethod(abc.ABC, BaseModel, Generic[T]):
|
|||
|
||||
return super().dict(exclude=exclude, **kwargs)
|
||||
|
||||
def build_response(self, data: Dict[str, Any]) -> Response[T]:
|
||||
def build_response(self, data: Dict[str, Any]) -> Response[TelegramType]:
|
||||
# noinspection PyTypeChecker
|
||||
return Response[self.__returning__](**data) # type: ignore
|
||||
|
||||
async def emit(self, bot: Bot) -> T:
|
||||
async def emit(self, bot: Bot) -> TelegramType:
|
||||
return await bot(self)
|
||||
|
||||
def __await__(self) -> Generator[Any, None, T]:
|
||||
def __await__(self) -> Generator[Any, None, TelegramType]:
|
||||
from aiogram.client.bot import Bot
|
||||
|
||||
bot = Bot.get_current(no_error=False)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ from .base import UNSET, TelegramObject
|
|||
if TYPE_CHECKING: # pragma: no cover
|
||||
from ..methods import (
|
||||
CopyMessage,
|
||||
DeleteMessage,
|
||||
EditMessageCaption,
|
||||
EditMessageText,
|
||||
SendAnimation,
|
||||
SendAudio,
|
||||
SendContact,
|
||||
|
|
@ -1714,6 +1717,49 @@ class Message(TelegramObject):
|
|||
reply_markup=reply_markup,
|
||||
)
|
||||
|
||||
def edit_text(
|
||||
self,
|
||||
text: str,
|
||||
parse_mode: Optional[str] = UNSET,
|
||||
entities: Optional[List[MessageEntity]] = None,
|
||||
disable_web_page_preview: Optional[bool] = None,
|
||||
reply_markup: Optional[InlineKeyboardMarkup] = None,
|
||||
) -> EditMessageText:
|
||||
from ..methods import EditMessageText
|
||||
|
||||
return EditMessageText(
|
||||
chat_id=self.chat.id,
|
||||
message_id=self.message_id,
|
||||
text=text,
|
||||
parse_mode=parse_mode,
|
||||
entities=entities,
|
||||
disable_web_page_preview=disable_web_page_preview,
|
||||
reply_markup=reply_markup,
|
||||
)
|
||||
|
||||
def edit_caption(
|
||||
self,
|
||||
caption: str,
|
||||
parse_mode: Optional[str] = UNSET,
|
||||
caption_entities: Optional[List[MessageEntity]] = None,
|
||||
reply_markup: Optional[InlineKeyboardMarkup] = None,
|
||||
) -> EditMessageCaption:
|
||||
from ..methods import EditMessageCaption
|
||||
|
||||
return EditMessageCaption(
|
||||
chat_id=self.chat.id,
|
||||
message_id=self.message_id,
|
||||
caption=caption,
|
||||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
reply_markup=reply_markup,
|
||||
)
|
||||
|
||||
def delete(self) -> DeleteMessage:
|
||||
from ..methods import DeleteMessage
|
||||
|
||||
return DeleteMessage(chat_id=self.chat.id, message_id=self.message_id)
|
||||
|
||||
def get_url(self, force_private: bool = False) -> Optional[str]:
|
||||
"""
|
||||
Returns message URL. Cannot be used in private (one-to-one) chats.
|
||||
|
|
|
|||
|
|
@ -1,563 +0,0 @@
|
|||
"""
|
||||
- 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
|
||||
|
||||
- AIOGramWarning
|
||||
- TimeoutWarning
|
||||
"""
|
||||
|
||||
|
||||
class TelegramAPIError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# _PREFIXES = ["error: ", "[error]: ", "bad request: ", "conflict: ", "not found: "]
|
||||
#
|
||||
|
||||
# def _clean_message(text):
|
||||
# for prefix in _PREFIXES:
|
||||
# if text.startswith(prefix):
|
||||
# text = text[len(prefix) :]
|
||||
# return (text[0].upper() + text[1:]).strip()
|
||||
#
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
# class _MatchErrorMixin:
|
||||
# match = ""
|
||||
# text = None
|
||||
#
|
||||
# __subclasses = []
|
||||
#
|
||||
# def __init_subclass__(cls, **kwargs):
|
||||
# super(_MatchErrorMixin, cls).__init_subclass__(**kwargs)
|
||||
# # cls.match = cls.match.lower() if cls.match else ''
|
||||
# if not hasattr(cls, f"_{cls.__name__}__group"):
|
||||
# cls.__subclasses.append(cls)
|
||||
#
|
||||
# @classmethod
|
||||
# def check(cls, message) -> bool:
|
||||
# """
|
||||
# Compare pattern with message
|
||||
#
|
||||
# :param message: always must be in lowercase
|
||||
# :return: bool
|
||||
# """
|
||||
# return cls.match.lower() in message
|
||||
#
|
||||
# @classmethod
|
||||
# def detect(cls, description):
|
||||
# description = description.lower()
|
||||
# for err in cls.__subclasses:
|
||||
# if err is cls:
|
||||
# continue
|
||||
# if err.check(description):
|
||||
# raise err(cls.text or description)
|
||||
# raise cls(description)
|
||||
#
|
||||
#
|
||||
# class AIOGramWarning(Warning):
|
||||
# pass
|
||||
#
|
||||
#
|
||||
# class TimeoutWarning(AIOGramWarning):
|
||||
# pass
|
||||
#
|
||||
#
|
||||
# class FSMStorageWarning(AIOGramWarning):
|
||||
# pass
|
||||
#
|
||||
#
|
||||
# class ValidationError(TelegramAPIError):
|
||||
# pass
|
||||
#
|
||||
#
|
||||
# class BadRequest(TelegramAPIError, _MatchErrorMixin):
|
||||
# __group = True
|
||||
#
|
||||
#
|
||||
# class MessageError(BadRequest):
|
||||
# __group = True
|
||||
#
|
||||
#
|
||||
# class MessageNotModified(MessageError):
|
||||
# """
|
||||
# Will be raised when you try to set new text is equals to current text.
|
||||
# """
|
||||
#
|
||||
# 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"
|
||||
#
|
||||
#
|
||||
# class MessageToDeleteNotFound(MessageError):
|
||||
# """
|
||||
# Will be raised when you try to delete very old or deleted or unknown message.
|
||||
# """
|
||||
#
|
||||
# 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"
|
||||
#
|
||||
#
|
||||
# class MessageIdentifierNotSpecified(MessageError):
|
||||
# match = "message identifier is not specified"
|
||||
#
|
||||
#
|
||||
# class MessageTextIsEmpty(MessageError):
|
||||
# match = "Message text is empty"
|
||||
#
|
||||
#
|
||||
# class MessageCantBeEdited(MessageError):
|
||||
# match = "message can't be edited"
|
||||
#
|
||||
#
|
||||
# class MessageCantBeDeleted(MessageError):
|
||||
# match = "message can't be deleted"
|
||||
#
|
||||
#
|
||||
# class MessageToEditNotFound(MessageError):
|
||||
# match = "message to edit not found"
|
||||
#
|
||||
#
|
||||
# class MessageIsTooLong(MessageError):
|
||||
# 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"
|
||||
#
|
||||
#
|
||||
# class ObjectExpectedAsReplyMarkup(BadRequest):
|
||||
# match = "object expected as reply markup"
|
||||
#
|
||||
#
|
||||
# class InlineKeyboardExpected(BadRequest):
|
||||
# match = "inline keyboard expected"
|
||||
#
|
||||
#
|
||||
# class PollError(BadRequest):
|
||||
# __group = True
|
||||
#
|
||||
#
|
||||
# class PollCantBeStopped(PollError):
|
||||
# match = "poll can't be stopped"
|
||||
#
|
||||
#
|
||||
# class PollHasAlreadyBeenClosed(PollError):
|
||||
# match = "poll has already been closed"
|
||||
#
|
||||
#
|
||||
# class PollsCantBeSentToPrivateChats(PollError):
|
||||
# match = "polls can't be sent to private chats"
|
||||
#
|
||||
#
|
||||
# class PollSizeError(PollError):
|
||||
# __group = True
|
||||
#
|
||||
#
|
||||
# class PollMustHaveMoreOptions(PollSizeError):
|
||||
# match = "poll must have at least 2 option"
|
||||
#
|
||||
#
|
||||
# class PollCantHaveMoreOptions(PollSizeError):
|
||||
# match = "poll can't have more than 10 options"
|
||||
#
|
||||
#
|
||||
# class PollOptionsMustBeNonEmpty(PollSizeError):
|
||||
# match = "poll options must be non-empty"
|
||||
#
|
||||
#
|
||||
# class PollQuestionMustBeNonEmpty(PollSizeError):
|
||||
# match = "poll question must be non-empty"
|
||||
#
|
||||
#
|
||||
# class PollOptionsLengthTooLong(PollSizeError):
|
||||
# match = "poll options length must not exceed 100"
|
||||
#
|
||||
#
|
||||
# class PollQuestionLengthTooLong(PollSizeError):
|
||||
# match = "poll question length must not exceed 255"
|
||||
#
|
||||
#
|
||||
# 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"
|
||||
#
|
||||
#
|
||||
# class MessageIsNotAPoll(PollError, MessageError):
|
||||
# """
|
||||
# Will be raised when you try to stop poll with message without poll
|
||||
# """
|
||||
#
|
||||
# match = "message is not a poll"
|
||||
#
|
||||
#
|
||||
# class ChatNotFound(BadRequest):
|
||||
# match = "chat not found"
|
||||
#
|
||||
#
|
||||
# class ChatIdIsEmpty(BadRequest):
|
||||
# match = "chat_id is empty"
|
||||
#
|
||||
#
|
||||
# class InvalidUserId(BadRequest):
|
||||
# match = "user_id_invalid"
|
||||
# text = "Invalid user id"
|
||||
#
|
||||
#
|
||||
# class ChatDescriptionIsNotModified(BadRequest):
|
||||
# match = "chat description is not modified"
|
||||
#
|
||||
#
|
||||
# class InvalidQueryID(BadRequest):
|
||||
# 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"
|
||||
#
|
||||
#
|
||||
# class InvalidHTTPUrlContent(BadRequest):
|
||||
# match = "Failed to get HTTP URL content"
|
||||
#
|
||||
#
|
||||
# class ButtonURLInvalid(BadRequest):
|
||||
# match = "BUTTON_URL_INVALID"
|
||||
# text = "Button URL invalid"
|
||||
#
|
||||
#
|
||||
# class URLHostIsEmpty(BadRequest):
|
||||
# match = "URL host is empty"
|
||||
#
|
||||
#
|
||||
# class StartParamInvalid(BadRequest):
|
||||
# match = "START_PARAM_INVALID"
|
||||
# text = "Start param invalid"
|
||||
#
|
||||
#
|
||||
# class ButtonDataInvalid(BadRequest):
|
||||
# match = "BUTTON_DATA_INVALID"
|
||||
# text = "Button data invalid"
|
||||
#
|
||||
#
|
||||
# class WrongFileIdentifier(BadRequest):
|
||||
# match = "wrong file identifier/HTTP URL specified"
|
||||
#
|
||||
#
|
||||
# class GroupDeactivated(BadRequest):
|
||||
# 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"
|
||||
#
|
||||
#
|
||||
# class InvalidStickersSet(BadRequest):
|
||||
# match = "STICKERSET_INVALID"
|
||||
# text = "Stickers set is invalid"
|
||||
#
|
||||
#
|
||||
# class NoStickerInRequest(BadRequest):
|
||||
# match = "there is no sticker in the request"
|
||||
#
|
||||
#
|
||||
# class ChatAdminRequired(BadRequest):
|
||||
# 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!"
|
||||
#
|
||||
#
|
||||
# class NotEnoughRightsToPinMessage(BadRequest):
|
||||
# match = "not enough rights to pin a message"
|
||||
#
|
||||
#
|
||||
# class MethodNotAvailableInPrivateChats(BadRequest):
|
||||
# match = "method is available only for supergroups and channel"
|
||||
#
|
||||
#
|
||||
# class CantDemoteChatCreator(BadRequest):
|
||||
# match = "can't demote chat creator"
|
||||
#
|
||||
#
|
||||
# class CantRestrictSelf(BadRequest):
|
||||
# match = "can't restrict self"
|
||||
# text = "Admin can't restrict self."
|
||||
#
|
||||
#
|
||||
# class NotEnoughRightsToRestrict(BadRequest):
|
||||
# match = "not enough rights to restrict/unrestrict chat member"
|
||||
#
|
||||
#
|
||||
# class PhotoDimensions(BadRequest):
|
||||
# match = "PHOTO_INVALID_DIMENSIONS"
|
||||
# text = "Invalid photo dimensions"
|
||||
#
|
||||
#
|
||||
# class UnavailableMembers(BadRequest):
|
||||
# match = "supergroup members are unavailable"
|
||||
#
|
||||
#
|
||||
# class TypeOfFileMismatch(BadRequest):
|
||||
# match = "type of file mismatch"
|
||||
#
|
||||
#
|
||||
# class WrongRemoteFileIdSpecified(BadRequest):
|
||||
# match = "wrong remote file id specified"
|
||||
#
|
||||
#
|
||||
# class PaymentProviderInvalid(BadRequest):
|
||||
# match = "PAYMENT_PROVIDER_INVALID"
|
||||
# text = "payment provider invalid"
|
||||
#
|
||||
#
|
||||
# class CurrencyTotalAmountInvalid(BadRequest):
|
||||
# match = "currency_total_amount_invalid"
|
||||
# text = "currency total amount invalid"
|
||||
#
|
||||
#
|
||||
# class BadWebhook(BadRequest):
|
||||
# __group = True
|
||||
#
|
||||
#
|
||||
# class WebhookRequireHTTPS(BadWebhook):
|
||||
# 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
|
||||
#
|
||||
#
|
||||
# class BadWebhookAddrInfo(BadWebhook):
|
||||
# match = "getaddrinfo: Temporary failure in name resolution"
|
||||
# text = "bad webhook: " + match
|
||||
#
|
||||
#
|
||||
# class BadWebhookNoAddressAssociatedWithHostname(BadWebhook):
|
||||
# match = "failed to resolve host: no address associated with hostname"
|
||||
#
|
||||
#
|
||||
# class CantParseUrl(BadRequest):
|
||||
# match = "can't parse URL"
|
||||
#
|
||||
#
|
||||
# class UnsupportedUrlProtocol(BadRequest):
|
||||
# match = "unsupported URL protocol"
|
||||
#
|
||||
#
|
||||
# class CantParseEntities(BadRequest):
|
||||
# match = "can't parse entities"
|
||||
#
|
||||
#
|
||||
# class ResultIdDuplicate(BadRequest):
|
||||
# match = "result_id_duplicate"
|
||||
# text = "Result ID duplicate"
|
||||
#
|
||||
#
|
||||
# class BotDomainInvalid(BadRequest):
|
||||
# match = "bot_domain_invalid"
|
||||
# text = "Invalid bot domain"
|
||||
#
|
||||
#
|
||||
# class NotFound(TelegramAPIError, _MatchErrorMixin):
|
||||
# __group = True
|
||||
#
|
||||
#
|
||||
# class MethodNotKnown(NotFound):
|
||||
# match = "method not found"
|
||||
#
|
||||
#
|
||||
# class ConflictError(TelegramAPIError, _MatchErrorMixin):
|
||||
# __group = True
|
||||
#
|
||||
#
|
||||
# class TerminatedByOtherGetUpdates(ConflictError):
|
||||
# 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"
|
||||
#
|
||||
#
|
||||
# class Unauthorized(TelegramAPIError, _MatchErrorMixin):
|
||||
# __group = True
|
||||
#
|
||||
#
|
||||
# class BotKicked(Unauthorized):
|
||||
# match = "bot was kicked from a chat"
|
||||
#
|
||||
#
|
||||
# class BotBlocked(Unauthorized):
|
||||
# match = "bot was blocked by the user"
|
||||
#
|
||||
#
|
||||
# class UserDeactivated(Unauthorized):
|
||||
# match = "user is deactivated"
|
||||
#
|
||||
#
|
||||
# class CantInitiateConversation(Unauthorized):
|
||||
# match = "bot can't initiate conversation with a user"
|
||||
#
|
||||
#
|
||||
# class CantTalkWithBots(Unauthorized):
|
||||
# match = "bot can't send messages to bots"
|
||||
#
|
||||
#
|
||||
# class NetworkError(TelegramAPIError):
|
||||
# pass
|
||||
#
|
||||
#
|
||||
# class RestartingTelegram(TelegramAPIError):
|
||||
# def __init__(self):
|
||||
# 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."
|
||||
# )
|
||||
# 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}."
|
||||
# )
|
||||
# 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.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)
|
||||
#
|
||||
# def __str__(self):
|
||||
# return (
|
||||
# f"Rate limit exceeded! (Limit: {self.rate} s, "
|
||||
# f"exceeded: {self.exceeded_count}, "
|
||||
# f"time delta: {round(self.delta, 3)} s)"
|
||||
# )
|
||||
0
aiogram/utils/exceptions/__init__.py
Normal file
0
aiogram/utils/exceptions/__init__.py
Normal file
5
aiogram/utils/exceptions/bad_request.py
Normal file
5
aiogram/utils/exceptions/bad_request.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from aiogram.utils.exceptions.base import DetailedTelegramAPIError
|
||||
|
||||
|
||||
class BadRequest(DetailedTelegramAPIError):
|
||||
pass
|
||||
40
aiogram/utils/exceptions/base.py
Normal file
40
aiogram/utils/exceptions/base.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
from typing import ClassVar, List, Match, Optional, TypeVar
|
||||
|
||||
from aiogram.methods import TelegramMethod
|
||||
from aiogram.methods.base import TelegramType
|
||||
|
||||
ErrorType = TypeVar("ErrorType")
|
||||
|
||||
|
||||
class TelegramAPIError(Exception):
|
||||
url: Optional[str] = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
method: TelegramMethod[TelegramType],
|
||||
message: str,
|
||||
) -> None:
|
||||
self.method = method
|
||||
self.message = message
|
||||
|
||||
def render_description(self) -> str:
|
||||
return self.message
|
||||
|
||||
def __str__(self) -> str:
|
||||
message = [self.render_description()]
|
||||
if self.url:
|
||||
message.append(f"(background on this error at: {self.url})")
|
||||
return "\n".join(message)
|
||||
|
||||
|
||||
class DetailedTelegramAPIError(TelegramAPIError):
|
||||
patterns: ClassVar[List[str]]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
method: TelegramMethod[TelegramType],
|
||||
message: str,
|
||||
match: Match[str],
|
||||
) -> None:
|
||||
super().__init__(method=method, message=message)
|
||||
self.match: Match[str] = match
|
||||
0
aiogram/utils/exceptions/conflict.py
Normal file
0
aiogram/utils/exceptions/conflict.py
Normal file
93
aiogram/utils/exceptions/exceptions.py
Normal file
93
aiogram/utils/exceptions/exceptions.py
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
from textwrap import indent
|
||||
from typing import Match
|
||||
|
||||
from aiogram.methods.base import TelegramMethod, TelegramType
|
||||
from aiogram.utils.exceptions.base import DetailedTelegramAPIError
|
||||
from aiogram.utils.exceptions.util import mark_line
|
||||
|
||||
|
||||
class BadRequest(DetailedTelegramAPIError):
|
||||
pass
|
||||
|
||||
|
||||
class CantParseEntities(BadRequest):
|
||||
pass
|
||||
|
||||
|
||||
class CantParseEntitiesStartTag(CantParseEntities):
|
||||
patterns = [
|
||||
"Bad Request: can't parse entities: Can't find end tag corresponding to start tag (?P<tag>.+)"
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
method: TelegramMethod[TelegramType],
|
||||
message: str,
|
||||
match: Match[str],
|
||||
) -> None:
|
||||
super().__init__(method=method, message=message, match=match)
|
||||
self.tag: str = match.group("tag")
|
||||
|
||||
|
||||
class CantParseEntitiesUnmatchedTags(CantParseEntities):
|
||||
patterns = [
|
||||
r'Bad Request: can\'t parse entities: Unmatched end tag at byte offset (?P<offset>\d), expected "</(?P<expected>\w+)>", found "</(?P<found>\w+)>"'
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
method: TelegramMethod[TelegramType],
|
||||
message: str,
|
||||
match: Match[str],
|
||||
) -> None:
|
||||
super().__init__(method=method, message=message, match=match)
|
||||
self.offset: int = int(match.group("offset"))
|
||||
self.expected: str = match.group("expected")
|
||||
self.found: str = match.group("found")
|
||||
|
||||
|
||||
class CantParseEntitiesUnclosed(CantParseEntities):
|
||||
patterns = [
|
||||
"Bad Request: can't parse entities: Unclosed start tag at byte offset (?P<offset>.+)"
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
method: TelegramMethod[TelegramType],
|
||||
message: str,
|
||||
match: Match[str],
|
||||
) -> None:
|
||||
super().__init__(method=method, message=message, match=match)
|
||||
self.offset: int = int(match.group("offset"))
|
||||
|
||||
def __str__(self) -> str:
|
||||
message = [self.message]
|
||||
text = getattr(self.method, "text", None) or getattr(self.method, "caption", None)
|
||||
if text:
|
||||
message.extend(["Example:", indent(mark_line(text, self.offset), prefix=" ")])
|
||||
return "\n".join(message)
|
||||
|
||||
|
||||
class CantParseEntitiesUnsupportedTag(CantParseEntities):
|
||||
patterns = [
|
||||
r'Bad Request: can\'t parse entities: Unsupported start tag "(?P<tag>.+)" at byte offset (?P<offset>\d+)'
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
method: TelegramMethod[TelegramType],
|
||||
message: str,
|
||||
match: Match[str],
|
||||
) -> None:
|
||||
super().__init__(method=method, message=message, match=match)
|
||||
self.offset = int(match.group("offset"))
|
||||
self.tag = match.group("tag")
|
||||
|
||||
def __str__(self) -> str:
|
||||
message = [self.message]
|
||||
text = getattr(self.method, "text", None) or getattr(self.method, "caption", None)
|
||||
if text:
|
||||
message.extend(
|
||||
["Example:", indent(mark_line(text, self.offset, len(self.tag)), prefix=" ")]
|
||||
)
|
||||
return "\n".join(message)
|
||||
5
aiogram/utils/exceptions/network.py
Normal file
5
aiogram/utils/exceptions/network.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from aiogram.utils.exceptions.base import DetailedTelegramAPIError
|
||||
|
||||
|
||||
class NetworkError(DetailedTelegramAPIError):
|
||||
pass
|
||||
5
aiogram/utils/exceptions/not_found.py
Normal file
5
aiogram/utils/exceptions/not_found.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from aiogram.utils.exceptions.base import DetailedTelegramAPIError
|
||||
|
||||
|
||||
class NotFound(DetailedTelegramAPIError):
|
||||
pass
|
||||
0
aiogram/utils/exceptions/server.py
Normal file
0
aiogram/utils/exceptions/server.py
Normal file
46
aiogram/utils/exceptions/special.py
Normal file
46
aiogram/utils/exceptions/special.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
from typing import Optional
|
||||
|
||||
from aiogram.methods import TelegramMethod
|
||||
from aiogram.methods.base import TelegramType
|
||||
from aiogram.utils.exceptions.base import TelegramAPIError
|
||||
|
||||
|
||||
class RetryAfter(TelegramAPIError):
|
||||
url = "https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
method: TelegramMethod[TelegramType],
|
||||
message: str,
|
||||
retry_after: int,
|
||||
) -> None:
|
||||
super().__init__(method=method, message=message)
|
||||
self.retry_after = retry_after
|
||||
|
||||
def render_description(self) -> str:
|
||||
description = f"Flood control exceeded on method {type(self.method).__name__!r}"
|
||||
if chat_id := getattr(self.method, "chat_id", None):
|
||||
description += f" in chat {chat_id}"
|
||||
description += f". Retry in {self.retry_after} seconds."
|
||||
return description
|
||||
|
||||
|
||||
class MigrateToChat(TelegramAPIError):
|
||||
url = "https://core.telegram.org/bots/api#responseparameters"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
method: TelegramMethod[TelegramType],
|
||||
message: str,
|
||||
migrate_to_chat_id: int,
|
||||
) -> None:
|
||||
super().__init__(method=method, message=message)
|
||||
self.migrate_to_chat_id = migrate_to_chat_id
|
||||
|
||||
def render_message(self) -> Optional[str]:
|
||||
description = (
|
||||
f"The group has been migrated to a supergroup with id {self.migrate_to_chat_id}"
|
||||
)
|
||||
if chat_id := getattr(self.method, "chat_id", None):
|
||||
description += f" from {chat_id}"
|
||||
return description
|
||||
0
aiogram/utils/exceptions/unauthorized.py
Normal file
0
aiogram/utils/exceptions/unauthorized.py
Normal file
20
aiogram/utils/exceptions/util.py
Normal file
20
aiogram/utils/exceptions/util.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
def mark_line(text: str, offset: int, length: int = 1) -> str:
|
||||
try:
|
||||
if offset > 0 and (new_line_pos := text[:offset].rindex("\n")):
|
||||
text = "..." + text[:new_line_pos]
|
||||
offset -= new_line_pos - 3
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if offset > 10:
|
||||
text = "..." + text[offset - 10 :]
|
||||
offset = 13
|
||||
|
||||
mark = " " * offset
|
||||
mark += "^" * length
|
||||
try:
|
||||
if new_line_pos := text[len(mark) :].index("\n"):
|
||||
text = text[:new_line_pos].rstrip() + "..."
|
||||
except ValueError:
|
||||
pass
|
||||
return text + "\n" + mark
|
||||
|
|
@ -1,8 +1,16 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from itertools import chain
|
||||
from itertools import cycle as repeat_all
|
||||
from typing import Any, Generator, Generic, Iterable, List, Optional, Type, TypeVar
|
||||
from typing import Any, Generator, Generic, Iterable, List, Optional, Type, TypeVar, Union
|
||||
|
||||
from aiogram.types import InlineKeyboardButton, KeyboardButton
|
||||
from aiogram.dispatcher.filters.callback_data import CallbackData
|
||||
from aiogram.types import (
|
||||
InlineKeyboardButton,
|
||||
InlineKeyboardMarkup,
|
||||
KeyboardButton,
|
||||
ReplyKeyboardMarkup,
|
||||
)
|
||||
|
||||
ButtonType = TypeVar("ButtonType", InlineKeyboardButton, KeyboardButton)
|
||||
T = TypeVar("T")
|
||||
|
|
@ -11,7 +19,7 @@ MIN_WIDTH = 1
|
|||
MAX_BUTTONS = 100
|
||||
|
||||
|
||||
class MarkupConstructor(Generic[ButtonType]):
|
||||
class KeyboardConstructor(Generic[ButtonType]):
|
||||
def __init__(
|
||||
self, button_type: Type[ButtonType], markup: Optional[List[List[ButtonType]]] = None
|
||||
) -> None:
|
||||
|
|
@ -106,7 +114,7 @@ class MarkupConstructor(Generic[ButtonType]):
|
|||
raise ValueError(f"Row size {size} are not allowed")
|
||||
return size
|
||||
|
||||
def copy(self: "MarkupConstructor[ButtonType]") -> "MarkupConstructor[ButtonType]":
|
||||
def copy(self: "KeyboardConstructor[ButtonType]") -> "KeyboardConstructor[ButtonType]":
|
||||
"""
|
||||
Make full copy of current constructor with markup
|
||||
|
||||
|
|
@ -120,7 +128,7 @@ class MarkupConstructor(Generic[ButtonType]):
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
>>> constructor = MarkupConstructor(button_type=InlineKeyboardButton)
|
||||
>>> constructor = KeyboardConstructor(button_type=InlineKeyboardButton)
|
||||
>>> ... # Add buttons to constructor
|
||||
>>> markup = InlineKeyboardMarkup(inline_keyboard=constructor.export())
|
||||
|
||||
|
|
@ -128,7 +136,7 @@ class MarkupConstructor(Generic[ButtonType]):
|
|||
"""
|
||||
return self._markup.copy()
|
||||
|
||||
def add(self, *buttons: ButtonType) -> "MarkupConstructor[ButtonType]":
|
||||
def add(self, *buttons: ButtonType) -> "KeyboardConstructor[ButtonType]":
|
||||
"""
|
||||
Add one or many buttons to markup.
|
||||
|
||||
|
|
@ -153,7 +161,9 @@ class MarkupConstructor(Generic[ButtonType]):
|
|||
self._markup = markup
|
||||
return self
|
||||
|
||||
def row(self, *buttons: ButtonType, width: int = MAX_WIDTH) -> "MarkupConstructor[ButtonType]":
|
||||
def row(
|
||||
self, *buttons: ButtonType, width: int = MAX_WIDTH
|
||||
) -> "KeyboardConstructor[ButtonType]":
|
||||
"""
|
||||
Add row to markup
|
||||
|
||||
|
|
@ -170,7 +180,7 @@ class MarkupConstructor(Generic[ButtonType]):
|
|||
)
|
||||
return self
|
||||
|
||||
def adjust(self, *sizes: int, repeat: bool = False) -> "MarkupConstructor[ButtonType]":
|
||||
def adjust(self, *sizes: int, repeat: bool = False) -> "KeyboardConstructor[ButtonType]":
|
||||
"""
|
||||
Adjust previously added buttons to specific row sizes.
|
||||
|
||||
|
|
@ -202,10 +212,17 @@ class MarkupConstructor(Generic[ButtonType]):
|
|||
self._markup = markup
|
||||
return self
|
||||
|
||||
def button(self, **kwargs: Any) -> "MarkupConstructor[ButtonType]":
|
||||
def button(self, **kwargs: Any) -> "KeyboardConstructor[ButtonType]":
|
||||
if isinstance(callback_data := kwargs.get("callback_data", None), CallbackData):
|
||||
kwargs["callback_data"] = callback_data.pack()
|
||||
button = self._button_type(**kwargs)
|
||||
return self.add(button)
|
||||
|
||||
def as_markup(self, **kwargs: Any) -> Union[InlineKeyboardMarkup, ReplyKeyboardMarkup]:
|
||||
if self._button_type is ReplyKeyboardMarkup:
|
||||
return ReplyKeyboardMarkup(keyboard=self.export(), **kwargs)
|
||||
return InlineKeyboardMarkup(inline_keyboard=self.export())
|
||||
|
||||
|
||||
def repeat_last(items: Iterable[T]) -> Generator[T, None, None]:
|
||||
items_iter = iter(items)
|
||||
|
|
@ -21,4 +21,4 @@ comment:
|
|||
require_changes: no
|
||||
branches:
|
||||
- dev-3.x
|
||||
after_n_builds: 8
|
||||
after_n_builds: 6
|
||||
|
|
|
|||
|
|
@ -1,39 +1,46 @@
|
|||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiogram import Bot, Dispatcher, types
|
||||
from aiogram.dispatcher.handler import MessageHandler
|
||||
from aiogram.types import Message
|
||||
|
||||
TOKEN = "42:TOKEN"
|
||||
dp = Dispatcher()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@dp.message(commands=["start"])
|
||||
class MyHandler(MessageHandler):
|
||||
|
||||
@dp.message(commands={"start"})
|
||||
async def command_start_handler(message: Message) -> None:
|
||||
"""
|
||||
This handler receive messages with `/start` command
|
||||
|
||||
Usage of Class-based handlers
|
||||
"""
|
||||
|
||||
async def handle(self) -> Any:
|
||||
await self.event.answer(f"<b>Hello, {self.from_user.full_name}!</b>")
|
||||
# Most of event objects has an aliases for API methods to be called in event context
|
||||
# For example if you want to answer to incoming message you can use `message.answer(...)` alias
|
||||
# and the target chat will be passed to :ref:`aiogram.methods.send_message.SendMessage` method automatically
|
||||
# or call API method directly via Bot instance: `bot.send_message(chat_id=message.chat.id, ...)`
|
||||
await message.answer(f"Hello, <b>{message.from_user.full_name}!</b>")
|
||||
|
||||
|
||||
@dp.message(content_types=[types.ContentType.ANY])
|
||||
async def echo_handler(message: types.Message, bot: Bot) -> Any:
|
||||
@dp.message()
|
||||
async def echo_handler(message: types.Message) -> Any:
|
||||
"""
|
||||
Handler will forward received message back to the sender
|
||||
|
||||
Usage of Function-based handlers
|
||||
By default message handler will handle all message types (like text, photo, sticker and etc.)
|
||||
"""
|
||||
|
||||
await bot.forward_message(
|
||||
from_chat_id=message.chat.id, chat_id=message.chat.id, message_id=message.message_id
|
||||
)
|
||||
try:
|
||||
# Send copy of the received message
|
||||
await message.send_copy(chat_id=message.chat.id)
|
||||
except TypeError:
|
||||
# But not all the types is supported to be copied so need to handle it
|
||||
await message.answer("Nice try!")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# Initialize Bot instance with an default parse mode which will be passed to all API calls
|
||||
bot = Bot(TOKEN, parse_mode="HTML")
|
||||
# And the run events dispatching
|
||||
dp.run_polling(bot)
|
||||
|
||||
|
||||
|
|
|
|||
111
examples/finite_state_machine.py
Normal file
111
examples/finite_state_machine.py
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from os import getenv
|
||||
|
||||
from aiogram import Bot, Dispatcher, F
|
||||
from aiogram.dispatcher.filters import Command
|
||||
from aiogram.dispatcher.fsm.context import FSMContext
|
||||
from aiogram.dispatcher.fsm.state import State, StatesGroup
|
||||
from aiogram.types import Message, ReplyKeyboardRemove, ReplyKeyboardMarkup, KeyboardButton
|
||||
from aiogram.utils.markdown import hbold
|
||||
from aiogram.utils.markup import KeyboardConstructor
|
||||
|
||||
GENDERS = ["Male", "Female", "Helicopter", "Other"]
|
||||
|
||||
dp = Dispatcher()
|
||||
|
||||
|
||||
# States
|
||||
class Form(StatesGroup):
|
||||
name = State() # Will be represented in storage as 'Form:name'
|
||||
age = State() # Will be represented in storage as 'Form:age'
|
||||
gender = State() # Will be represented in storage as 'Form:gender'
|
||||
|
||||
|
||||
@dp.message(Command(commands=["start"]))
|
||||
async def cmd_start(message: Message, state: FSMContext):
|
||||
"""
|
||||
Conversation's entry point
|
||||
"""
|
||||
# Set state
|
||||
await state.set_state(Form.name)
|
||||
await message.answer("Hi there! What's your name?")
|
||||
|
||||
|
||||
@dp.message(Command(commands=["cancel"]))
|
||||
@dp.message(F.text.lower() == "cancel")
|
||||
async def cancel_handler(message: Message, state: FSMContext):
|
||||
"""
|
||||
Allow user to cancel any action
|
||||
"""
|
||||
current_state = await state.get_state()
|
||||
if current_state is None:
|
||||
return
|
||||
|
||||
logging.info("Cancelling state %r", current_state)
|
||||
# Cancel state and inform user about it
|
||||
await state.clear()
|
||||
# And remove keyboard (just in case)
|
||||
await message.answer("Cancelled.", reply_markup=ReplyKeyboardRemove())
|
||||
|
||||
|
||||
@dp.message(Form.name)
|
||||
async def process_name(message: Message, state: FSMContext):
|
||||
"""
|
||||
Process user name
|
||||
"""
|
||||
await state.update_data(name=message.text)
|
||||
await state.set_state(Form.age)
|
||||
await message.answer("How old are you?")
|
||||
|
||||
|
||||
# Check age. Age gotta be digit
|
||||
@dp.message(Form.age, ~F.text.isdigit())
|
||||
async def process_age_invalid(message: Message):
|
||||
"""
|
||||
If age is invalid
|
||||
"""
|
||||
return await message.answer("Age gotta be a number.\nHow old are you? (digits only)")
|
||||
|
||||
|
||||
@dp.message(Form.age)
|
||||
async def process_age(message: Message, state: FSMContext):
|
||||
# Update state and data
|
||||
await state.set_state(Form.gender)
|
||||
await state.update_data(age=int(message.text))
|
||||
|
||||
# Configure ReplyKeyboardMarkup
|
||||
constructor = KeyboardConstructor(KeyboardButton)
|
||||
constructor.add(*(KeyboardButton(text=text) for text in GENDERS)).adjust(2)
|
||||
markup = ReplyKeyboardMarkup(
|
||||
resize_keyboard=True, selective=True, keyboard=constructor.export()
|
||||
)
|
||||
await message.reply("What is your gender?", reply_markup=markup)
|
||||
|
||||
|
||||
@dp.message(Form.gender)
|
||||
async def process_gender(message: Message, state: FSMContext):
|
||||
data = await state.update_data(gender=message.text)
|
||||
await state.clear()
|
||||
|
||||
# And send message
|
||||
await message.answer(
|
||||
(
|
||||
f'Hi, nice to meet you, {hbold(data["name"])}\n'
|
||||
f'Age: {hbold(data["age"])}\n'
|
||||
f'Gender: {hbold(data["gender"])}\n'
|
||||
),
|
||||
reply_markup=ReplyKeyboardRemove(),
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
bot = Bot(token=getenv("TELEGRAM_TOKEN"), parse_mode="HTML")
|
||||
|
||||
await dp.start_polling(bot)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
|
||||
asyncio.run(main())
|
||||
329
poetry.lock
generated
329
poetry.lock
generated
|
|
@ -108,17 +108,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
|||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "20.3.0"
|
||||
version = "21.2.0"
|
||||
description = "Classes Without Boilerplate"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[package.extras]
|
||||
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"]
|
||||
docs = ["furo", "sphinx", "zope.interface"]
|
||||
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
|
||||
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"]
|
||||
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"]
|
||||
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
|
||||
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
|
||||
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
|
||||
|
||||
[[package]]
|
||||
name = "babel"
|
||||
|
|
@ -156,7 +156,7 @@ lxml = ["lxml"]
|
|||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "21.4b2"
|
||||
version = "21.5b1"
|
||||
description = "The uncompromising code formatter."
|
||||
category = "dev"
|
||||
optional = false
|
||||
|
|
@ -169,17 +169,15 @@ mypy-extensions = ">=0.4.3"
|
|||
pathspec = ">=0.8.1,<1"
|
||||
regex = ">=2020.1.8"
|
||||
toml = ">=0.10.1"
|
||||
typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\""}
|
||||
typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
|
||||
|
||||
[package.extras]
|
||||
colorama = ["colorama (>=0.4.3)"]
|
||||
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
|
||||
d = ["aiohttp (>=3.6.0)", "aiohttp-cors"]
|
||||
python2 = ["typed-ast (>=1.4.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "cfgv"
|
||||
version = "3.2.0"
|
||||
version = "3.3.0"
|
||||
description = "Validate configuration and produce human readable error messages."
|
||||
category = "dev"
|
||||
optional = false
|
||||
|
|
@ -195,11 +193,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
|||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "7.1.2"
|
||||
version = "8.0.1"
|
||||
description = "Composable command line interface toolkit"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
|
|
@ -217,12 +218,15 @@ category = "dev"
|
|||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
|
||||
|
||||
[package.dependencies]
|
||||
toml = {version = "*", optional = true, markers = "extra == \"toml\""}
|
||||
|
||||
[package.extras]
|
||||
toml = ["toml"]
|
||||
|
||||
[[package]]
|
||||
name = "decorator"
|
||||
version = "5.0.7"
|
||||
version = "5.0.9"
|
||||
description = "Decorators for Humans"
|
||||
category = "dev"
|
||||
optional = false
|
||||
|
|
@ -254,14 +258,13 @@ python-versions = "*"
|
|||
|
||||
[[package]]
|
||||
name = "flake8"
|
||||
version = "3.9.1"
|
||||
version = "3.9.2"
|
||||
description = "the modular source code checker: pep8 pyflakes and co"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||
|
||||
[package.dependencies]
|
||||
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
|
||||
mccabe = ">=0.6.0,<0.7.0"
|
||||
pycodestyle = ">=2.7.0,<2.8.0"
|
||||
pyflakes = ">=2.3.0,<2.4.0"
|
||||
|
|
@ -298,7 +301,7 @@ test = ["pytest", "pytest-cov", "pytest-xdist"]
|
|||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.2.3"
|
||||
version = "2.2.5"
|
||||
description = "File identification library for Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
|
|
@ -325,14 +328,13 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
|||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "4.0.0"
|
||||
version = "4.0.1"
|
||||
description = "Read metadata from Python packages"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
|
||||
zipp = ">=0.5"
|
||||
|
||||
[package.extras]
|
||||
|
|
@ -349,7 +351,7 @@ python-versions = "*"
|
|||
|
||||
[[package]]
|
||||
name = "ipython"
|
||||
version = "7.22.0"
|
||||
version = "7.23.1"
|
||||
description = "IPython: Productive Interactive Computing"
|
||||
category = "dev"
|
||||
optional = false
|
||||
|
|
@ -361,6 +363,7 @@ backcall = "*"
|
|||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
decorator = "*"
|
||||
jedi = ">=0.16"
|
||||
matplotlib-inline = "*"
|
||||
pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""}
|
||||
pickleshare = "*"
|
||||
prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0"
|
||||
|
|
@ -416,17 +419,17 @@ testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"]
|
|||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "2.11.3"
|
||||
version = "3.0.1"
|
||||
description = "A very fast and expressive template engine."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=0.23"
|
||||
MarkupSafe = ">=2.0"
|
||||
|
||||
[package.extras]
|
||||
i18n = ["Babel (>=0.8)"]
|
||||
i18n = ["Babel (>=2.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "livereload"
|
||||
|
|
@ -456,9 +459,6 @@ category = "dev"
|
|||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
|
||||
|
||||
[package.extras]
|
||||
testing = ["coverage", "pyyaml"]
|
||||
|
||||
|
|
@ -475,11 +475,22 @@ markdown = "*"
|
|||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "1.1.1"
|
||||
version = "2.0.1"
|
||||
description = "Safely add untrusted strings to HTML/XML markup."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "matplotlib-inline"
|
||||
version = "0.1.2"
|
||||
description = "Inline Matplotlib backend for Jupyter"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[package.dependencies]
|
||||
traitlets = "*"
|
||||
|
||||
[[package]]
|
||||
name = "mccabe"
|
||||
|
|
@ -587,15 +598,12 @@ category = "dev"
|
|||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[package.dependencies]
|
||||
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "tox"]
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "2.12.1"
|
||||
version = "2.13.0"
|
||||
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
||||
category = "dev"
|
||||
optional = false
|
||||
|
|
@ -604,7 +612,6 @@ python-versions = ">=3.6.1"
|
|||
[package.dependencies]
|
||||
cfgv = ">=2.0.0"
|
||||
identify = ">=1.0.0"
|
||||
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
|
||||
nodeenv = ">=0.11.1"
|
||||
pyyaml = ">=5.1"
|
||||
toml = "*"
|
||||
|
|
@ -647,7 +654,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
|||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "1.8.1"
|
||||
version = "1.8.2"
|
||||
description = "Data validation and settings management using python 3.6 type hinting"
|
||||
category = "main"
|
||||
optional = false
|
||||
|
|
@ -670,7 +677,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
|||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.8.1"
|
||||
version = "2.9.0"
|
||||
description = "Pygments is a syntax highlighting package written in Python."
|
||||
category = "main"
|
||||
optional = false
|
||||
|
|
@ -678,7 +685,7 @@ python-versions = ">=3.5"
|
|||
|
||||
[[package]]
|
||||
name = "pymdown-extensions"
|
||||
version = "8.1.1"
|
||||
version = "8.2"
|
||||
description = "Extension pack for Python Markdown."
|
||||
category = "dev"
|
||||
optional = false
|
||||
|
|
@ -697,7 +704,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
|||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "6.2.3"
|
||||
version = "6.2.4"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
|
|
@ -707,7 +714,6 @@ python-versions = ">=3.6"
|
|||
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
|
||||
attrs = ">=19.2.0"
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=0.12,<1.0.0a1"
|
||||
|
|
@ -733,14 +739,14 @@ testing = ["coverage", "hypothesis (>=5.7.1)"]
|
|||
|
||||
[[package]]
|
||||
name = "pytest-cov"
|
||||
version = "2.11.1"
|
||||
version = "2.12.0"
|
||||
description = "Pytest plugin for measuring coverage."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[package.dependencies]
|
||||
coverage = ">=5.2.1"
|
||||
coverage = {version = ">=5.2.1", extras = ["toml"]}
|
||||
pytest = ">=4.6"
|
||||
|
||||
[package.extras]
|
||||
|
|
@ -771,7 +777,7 @@ pytest = ">=2.9.0"
|
|||
|
||||
[[package]]
|
||||
name = "pytest-mock"
|
||||
version = "3.6.0"
|
||||
version = "3.6.1"
|
||||
description = "Thin-wrapper around the mock package for easier use with pytest"
|
||||
category = "dev"
|
||||
optional = false
|
||||
|
|
@ -795,7 +801,6 @@ python-versions = ">=3.5"
|
|||
attrs = ">=19.0"
|
||||
filelock = ">=3.0"
|
||||
mypy = [
|
||||
{version = ">=0.500", markers = "python_version < \"3.8\""},
|
||||
{version = ">=0.700", markers = "python_version >= \"3.8\" and python_version < \"3.9\""},
|
||||
{version = ">=0.780", markers = "python_version >= \"3.9\""},
|
||||
]
|
||||
|
|
@ -855,7 +860,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
|
|||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
category = "main"
|
||||
optional = false
|
||||
|
|
@ -1008,11 +1013,11 @@ test = ["pytest"]
|
|||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-htmlhelp"
|
||||
version = "1.0.3"
|
||||
version = "2.0.0"
|
||||
description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.extras]
|
||||
lint = ["flake8", "mypy", "docutils-stubs"]
|
||||
|
|
@ -1043,7 +1048,7 @@ test = ["pytest"]
|
|||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-serializinghtml"
|
||||
version = "1.1.4"
|
||||
version = "1.1.5"
|
||||
description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
|
||||
category = "main"
|
||||
optional = false
|
||||
|
|
@ -1093,7 +1098,7 @@ python-versions = "*"
|
|||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "3.7.4.3"
|
||||
version = "3.10.0.0"
|
||||
description = "Backported and Experimental Type Hints for Python 3.5+"
|
||||
category = "main"
|
||||
optional = false
|
||||
|
|
@ -1114,7 +1119,7 @@ test = ["aiohttp", "flake8 (>=3.8.4,<3.9.0)", "psutil", "pycodestyle (>=2.6.0,<2
|
|||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.4.3"
|
||||
version = "20.4.7"
|
||||
description = "Virtual Python Environment builder"
|
||||
category = "dev"
|
||||
optional = false
|
||||
|
|
@ -1124,7 +1129,6 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
|
|||
appdirs = ">=1.4.3,<2"
|
||||
distlib = ">=0.3.1,<1"
|
||||
filelock = ">=3.0.0,<4"
|
||||
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||
six = ">=1.9.0,<2"
|
||||
|
||||
[package.extras]
|
||||
|
|
@ -1150,7 +1154,6 @@ python-versions = ">=3.6"
|
|||
[package.dependencies]
|
||||
idna = ">=2.0"
|
||||
multidict = ">=4.0"
|
||||
typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
|
||||
|
||||
[[package]]
|
||||
name = "zipp"
|
||||
|
|
@ -1171,8 +1174,8 @@ proxy = ["aiohttp-socks"]
|
|||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.7"
|
||||
content-hash = "9a787135a6d8ed2a395c07246db424e9961281cf7e0dc00fc08507da3081143e"
|
||||
python-versions = "^3.8"
|
||||
content-hash = "2fcd44a8937b3ea48196c8eba8ceb0533281af34c884103bcc5b4f5f16b817d5"
|
||||
|
||||
[metadata.files]
|
||||
aiofiles = [
|
||||
|
|
@ -1254,8 +1257,8 @@ atomicwrites = [
|
|||
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
|
||||
]
|
||||
attrs = [
|
||||
{file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"},
|
||||
{file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"},
|
||||
{file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
|
||||
{file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
|
||||
]
|
||||
babel = [
|
||||
{file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"},
|
||||
|
|
@ -1271,20 +1274,20 @@ beautifulsoup4 = [
|
|||
{file = "beautifulsoup4-4.9.3.tar.gz", hash = "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25"},
|
||||
]
|
||||
black = [
|
||||
{file = "black-21.4b2-py3-none-any.whl", hash = "sha256:bff7067d8bc25eb21dcfdbc8c72f2baafd9ec6de4663241a52fb904b304d391f"},
|
||||
{file = "black-21.4b2.tar.gz", hash = "sha256:fc9bcf3b482b05c1f35f6a882c079dc01b9c7795827532f4cc43c0ec88067bbc"},
|
||||
{file = "black-21.5b1-py3-none-any.whl", hash = "sha256:8a60071a0043876a4ae96e6c69bd3a127dad2c1ca7c8083573eb82f92705d008"},
|
||||
{file = "black-21.5b1.tar.gz", hash = "sha256:23695358dbcb3deafe7f0a3ad89feee5999a46be5fec21f4f1d108be0bcdb3b1"},
|
||||
]
|
||||
cfgv = [
|
||||
{file = "cfgv-3.2.0-py2.py3-none-any.whl", hash = "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d"},
|
||||
{file = "cfgv-3.2.0.tar.gz", hash = "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1"},
|
||||
{file = "cfgv-3.3.0-py2.py3-none-any.whl", hash = "sha256:b449c9c6118fe8cca7fa5e00b9ec60ba08145d281d52164230a69211c5d597a1"},
|
||||
{file = "cfgv-3.3.0.tar.gz", hash = "sha256:9e600479b3b99e8af981ecdfc80a0296104ee610cab48a5ae4ffd0b668650eb1"},
|
||||
]
|
||||
chardet = [
|
||||
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
|
||||
{file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
|
||||
]
|
||||
click = [
|
||||
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
|
||||
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
|
||||
{file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"},
|
||||
{file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"},
|
||||
]
|
||||
colorama = [
|
||||
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
|
||||
|
|
@ -1345,8 +1348,8 @@ coverage = [
|
|||
{file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"},
|
||||
]
|
||||
decorator = [
|
||||
{file = "decorator-5.0.7-py3-none-any.whl", hash = "sha256:945d84890bb20cc4a2f4a31fc4311c0c473af65ea318617f13a7257c9a58bc98"},
|
||||
{file = "decorator-5.0.7.tar.gz", hash = "sha256:6f201a6c4dac3d187352661f508b9364ec8091217442c9478f1f83c003a0f060"},
|
||||
{file = "decorator-5.0.9-py3-none-any.whl", hash = "sha256:6e5c199c16f7a9f0e3a61a4a54b3d27e7dad0dbdde92b944426cb20914376323"},
|
||||
{file = "decorator-5.0.9.tar.gz", hash = "sha256:72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5"},
|
||||
]
|
||||
distlib = [
|
||||
{file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"},
|
||||
|
|
@ -1361,8 +1364,8 @@ filelock = [
|
|||
{file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"},
|
||||
]
|
||||
flake8 = [
|
||||
{file = "flake8-3.9.1-py2.py3-none-any.whl", hash = "sha256:3b9f848952dddccf635be78098ca75010f073bfe14d2c6bda867154bea728d2a"},
|
||||
{file = "flake8-3.9.1.tar.gz", hash = "sha256:1aa8990be1e689d96c745c5682b687ea49f2e05a443aff1f8251092b0014e378"},
|
||||
{file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"},
|
||||
{file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
|
||||
]
|
||||
flake8-html = [
|
||||
{file = "flake8-html-0.4.1.tar.gz", hash = "sha256:2fb436cbfe1e109275bc8fb7fdd0cb00e67b3b48cfeb397309b6b2c61eeb4cb4"},
|
||||
|
|
@ -1373,8 +1376,8 @@ furo = [
|
|||
{file = "furo-2020.12.30b24.tar.gz", hash = "sha256:30171899c9c06d692a778e6daf6cb2e5cbb05efc6006e1692e5e776007dc8a8c"},
|
||||
]
|
||||
identify = [
|
||||
{file = "identify-2.2.3-py2.py3-none-any.whl", hash = "sha256:398cb92a7599da0b433c65301a1b62b9b1f4bb8248719b84736af6c0b22289d6"},
|
||||
{file = "identify-2.2.3.tar.gz", hash = "sha256:4537474817e0bbb8cea3e5b7504b7de6d44e3f169a90846cbc6adb0fc8294502"},
|
||||
{file = "identify-2.2.5-py2.py3-none-any.whl", hash = "sha256:9c3ab58543c03bd794a1735e4552ef6dec49ec32053278130d525f0982447d47"},
|
||||
{file = "identify-2.2.5.tar.gz", hash = "sha256:bc1705694253763a3160b943316867792ec00ba7a0ee40b46e20aebaf4e0c46a"},
|
||||
]
|
||||
idna = [
|
||||
{file = "idna-3.1-py3-none-any.whl", hash = "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16"},
|
||||
|
|
@ -1385,16 +1388,16 @@ imagesize = [
|
|||
{file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"},
|
||||
]
|
||||
importlib-metadata = [
|
||||
{file = "importlib_metadata-4.0.0-py3-none-any.whl", hash = "sha256:19192b88d959336bfa6bdaaaef99aeafec179eca19c47c804e555703ee5f07ef"},
|
||||
{file = "importlib_metadata-4.0.0.tar.gz", hash = "sha256:2e881981c9748d7282b374b68e759c87745c25427b67ecf0cc67fb6637a1bff9"},
|
||||
{file = "importlib_metadata-4.0.1-py3-none-any.whl", hash = "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d"},
|
||||
{file = "importlib_metadata-4.0.1.tar.gz", hash = "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581"},
|
||||
]
|
||||
iniconfig = [
|
||||
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
||||
]
|
||||
ipython = [
|
||||
{file = "ipython-7.22.0-py3-none-any.whl", hash = "sha256:c0ce02dfaa5f854809ab7413c601c4543846d9da81010258ecdab299b542d199"},
|
||||
{file = "ipython-7.22.0.tar.gz", hash = "sha256:9c900332d4c5a6de534b4befeeb7de44ad0cc42e8327fa41b7685abde58cec74"},
|
||||
{file = "ipython-7.23.1-py3-none-any.whl", hash = "sha256:f78c6a3972dde1cc9e4041cbf4de583546314ba52d3c97208e5b6b2221a9cb7d"},
|
||||
{file = "ipython-7.23.1.tar.gz", hash = "sha256:714810a5c74f512b69d5f3b944c86e592cee0a5fb9c728e582f074610f6cf038"},
|
||||
]
|
||||
ipython-genutils = [
|
||||
{file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"},
|
||||
|
|
@ -1409,8 +1412,8 @@ jedi = [
|
|||
{file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"},
|
||||
]
|
||||
jinja2 = [
|
||||
{file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"},
|
||||
{file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"},
|
||||
{file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"},
|
||||
{file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"},
|
||||
]
|
||||
livereload = [
|
||||
{file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"},
|
||||
|
|
@ -1427,58 +1430,44 @@ markdown-include = [
|
|||
{file = "markdown-include-0.6.0.tar.gz", hash = "sha256:6f5d680e36f7780c7f0f61dca53ca581bd50d1b56137ddcd6353efafa0c3e4a2"},
|
||||
]
|
||||
markupsafe = [
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"},
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"},
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"},
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"},
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"},
|
||||
{file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"},
|
||||
{file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"},
|
||||
{file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"},
|
||||
{file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"},
|
||||
{file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"},
|
||||
{file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"},
|
||||
{file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"},
|
||||
{file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"},
|
||||
{file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"},
|
||||
{file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"},
|
||||
{file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"},
|
||||
{file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"},
|
||||
{file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"},
|
||||
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"},
|
||||
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"},
|
||||
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"},
|
||||
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"},
|
||||
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"},
|
||||
{file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"},
|
||||
{file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"},
|
||||
{file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"},
|
||||
{file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"},
|
||||
{file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"},
|
||||
{file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"},
|
||||
{file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"},
|
||||
{file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"},
|
||||
{file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"},
|
||||
{file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"},
|
||||
{file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
|
||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"},
|
||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"},
|
||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"},
|
||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"},
|
||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"},
|
||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"},
|
||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"},
|
||||
{file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"},
|
||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"},
|
||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"},
|
||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"},
|
||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"},
|
||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"},
|
||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"},
|
||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"},
|
||||
{file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"},
|
||||
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"},
|
||||
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"},
|
||||
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"},
|
||||
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"},
|
||||
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"},
|
||||
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"},
|
||||
{file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"},
|
||||
{file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"},
|
||||
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"},
|
||||
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"},
|
||||
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"},
|
||||
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"},
|
||||
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"},
|
||||
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"},
|
||||
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"},
|
||||
{file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"},
|
||||
{file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"},
|
||||
{file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"},
|
||||
]
|
||||
matplotlib-inline = [
|
||||
{file = "matplotlib-inline-0.1.2.tar.gz", hash = "sha256:f41d5ff73c9f5385775d5c0bc13b424535c8402fe70ea8210f93e11f3683993e"},
|
||||
{file = "matplotlib_inline-0.1.2-py3-none-any.whl", hash = "sha256:5cf1176f554abb4fa98cb362aa2b55c500147e4bdbb07e3fda359143e1da0811"},
|
||||
]
|
||||
mccabe = [
|
||||
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
|
||||
|
|
@ -1580,8 +1569,8 @@ pluggy = [
|
|||
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
|
||||
]
|
||||
pre-commit = [
|
||||
{file = "pre_commit-2.12.1-py2.py3-none-any.whl", hash = "sha256:70c5ec1f30406250b706eda35e868b87e3e4ba099af8787e3e8b4b01e84f4712"},
|
||||
{file = "pre_commit-2.12.1.tar.gz", hash = "sha256:900d3c7e1bf4cf0374bb2893c24c23304952181405b4d88c9c40b72bda1bb8a9"},
|
||||
{file = "pre_commit-2.13.0-py2.py3-none-any.whl", hash = "sha256:b679d0fddd5b9d6d98783ae5f10fd0c4c59954f375b70a58cbe1ce9bcf9809a4"},
|
||||
{file = "pre_commit-2.13.0.tar.gz", hash = "sha256:764972c60693dc668ba8e86eb29654ec3144501310f7198742a767bec385a378"},
|
||||
]
|
||||
prompt-toolkit = [
|
||||
{file = "prompt_toolkit-3.0.18-py3-none-any.whl", hash = "sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04"},
|
||||
|
|
@ -1600,56 +1589,56 @@ pycodestyle = [
|
|||
{file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"},
|
||||
]
|
||||
pydantic = [
|
||||
{file = "pydantic-1.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0c40162796fc8d0aa744875b60e4dc36834db9f2a25dbf9ba9664b1915a23850"},
|
||||
{file = "pydantic-1.8.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:fff29fe54ec419338c522b908154a2efabeee4f483e48990f87e189661f31ce3"},
|
||||
{file = "pydantic-1.8.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:fbfb608febde1afd4743c6822c19060a8dbdd3eb30f98e36061ba4973308059e"},
|
||||
{file = "pydantic-1.8.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:eb8ccf12295113ce0de38f80b25f736d62f0a8d87c6b88aca645f168f9c78771"},
|
||||
{file = "pydantic-1.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:20d42f1be7c7acc352b3d09b0cf505a9fab9deb93125061b376fbe1f06a5459f"},
|
||||
{file = "pydantic-1.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dde4ca368e82791de97c2ec019681ffb437728090c0ff0c3852708cf923e0c7d"},
|
||||
{file = "pydantic-1.8.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:3bbd023c981cbe26e6e21c8d2ce78485f85c2e77f7bab5ec15b7d2a1f491918f"},
|
||||
{file = "pydantic-1.8.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:830ef1a148012b640186bf4d9789a206c56071ff38f2460a32ae67ca21880eb8"},
|
||||
{file = "pydantic-1.8.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:fb77f7a7e111db1832ae3f8f44203691e15b1fa7e5a1cb9691d4e2659aee41c4"},
|
||||
{file = "pydantic-1.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:3bcb9d7e1f9849a6bdbd027aabb3a06414abd6068cb3b21c49427956cce5038a"},
|
||||
{file = "pydantic-1.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2287ebff0018eec3cc69b1d09d4b7cebf277726fa1bd96b45806283c1d808683"},
|
||||
{file = "pydantic-1.8.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:4bbc47cf7925c86a345d03b07086696ed916c7663cb76aa409edaa54546e53e2"},
|
||||
{file = "pydantic-1.8.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:6388ef4ef1435364c8cc9a8192238aed030595e873d8462447ccef2e17387125"},
|
||||
{file = "pydantic-1.8.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:dd4888b300769ecec194ca8f2699415f5f7760365ddbe243d4fd6581485fa5f0"},
|
||||
{file = "pydantic-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:8fbb677e4e89c8ab3d450df7b1d9caed23f254072e8597c33279460eeae59b99"},
|
||||
{file = "pydantic-1.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2f2736d9a996b976cfdfe52455ad27462308c9d3d0ae21a2aa8b4cd1a78f47b9"},
|
||||
{file = "pydantic-1.8.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:3114d74329873af0a0e8004627f5389f3bb27f956b965ddd3e355fe984a1789c"},
|
||||
{file = "pydantic-1.8.1-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:258576f2d997ee4573469633592e8b99aa13bda182fcc28e875f866016c8e07e"},
|
||||
{file = "pydantic-1.8.1-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:c17a0b35c854049e67c68b48d55e026c84f35593c66d69b278b8b49e2484346f"},
|
||||
{file = "pydantic-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:e8bc082afef97c5fd3903d05c6f7bb3a6af9fc18631b4cc9fedeb4720efb0c58"},
|
||||
{file = "pydantic-1.8.1-py3-none-any.whl", hash = "sha256:e3f8790c47ac42549dc8b045a67b0ca371c7f66e73040d0197ce6172b385e520"},
|
||||
{file = "pydantic-1.8.1.tar.gz", hash = "sha256:26cf3cb2e68ec6c0cfcb6293e69fb3450c5fd1ace87f46b64f678b0d29eac4c3"},
|
||||
{file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"},
|
||||
{file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"},
|
||||
{file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e"},
|
||||
{file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840"},
|
||||
{file = "pydantic-1.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b"},
|
||||
{file = "pydantic-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20"},
|
||||
{file = "pydantic-1.8.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb"},
|
||||
{file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1"},
|
||||
{file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23"},
|
||||
{file = "pydantic-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287"},
|
||||
{file = "pydantic-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd"},
|
||||
{file = "pydantic-1.8.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505"},
|
||||
{file = "pydantic-1.8.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e"},
|
||||
{file = "pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820"},
|
||||
{file = "pydantic-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3"},
|
||||
{file = "pydantic-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316"},
|
||||
{file = "pydantic-1.8.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62"},
|
||||
{file = "pydantic-1.8.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f"},
|
||||
{file = "pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b"},
|
||||
{file = "pydantic-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3"},
|
||||
{file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"},
|
||||
{file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"},
|
||||
]
|
||||
pyflakes = [
|
||||
{file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"},
|
||||
{file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
|
||||
]
|
||||
pygments = [
|
||||
{file = "Pygments-2.8.1-py3-none-any.whl", hash = "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8"},
|
||||
{file = "Pygments-2.8.1.tar.gz", hash = "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94"},
|
||||
{file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"},
|
||||
{file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"},
|
||||
]
|
||||
pymdown-extensions = [
|
||||
{file = "pymdown-extensions-8.1.1.tar.gz", hash = "sha256:632371fa3bf1b21a0e3f4063010da59b41db049f261f4c0b0872069a9b6d1735"},
|
||||
{file = "pymdown_extensions-8.1.1-py3-none-any.whl", hash = "sha256:478b2c04513fbb2db61688d5f6e9030a92fb9be14f1f383535c43f7be9dff95b"},
|
||||
{file = "pymdown-extensions-8.2.tar.gz", hash = "sha256:b6daa94aad9e1310f9c64c8b1f01e4ce82937ab7eb53bfc92876a97aca02a6f4"},
|
||||
{file = "pymdown_extensions-8.2-py3-none-any.whl", hash = "sha256:141452d8ed61165518f2c923454bf054866b85cf466feedb0eb68f04acdc2560"},
|
||||
]
|
||||
pyparsing = [
|
||||
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
|
||||
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
|
||||
]
|
||||
pytest = [
|
||||
{file = "pytest-6.2.3-py3-none-any.whl", hash = "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc"},
|
||||
{file = "pytest-6.2.3.tar.gz", hash = "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634"},
|
||||
{file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"},
|
||||
{file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"},
|
||||
]
|
||||
pytest-asyncio = [
|
||||
{file = "pytest-asyncio-0.15.1.tar.gz", hash = "sha256:2564ceb9612bbd560d19ca4b41347b54e7835c2f792c504f698e05395ed63f6f"},
|
||||
{file = "pytest_asyncio-0.15.1-py3-none-any.whl", hash = "sha256:3042bcdf1c5d978f6b74d96a151c4cfb9dcece65006198389ccd7e6c60eb1eea"},
|
||||
]
|
||||
pytest-cov = [
|
||||
{file = "pytest-cov-2.11.1.tar.gz", hash = "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7"},
|
||||
{file = "pytest_cov-2.11.1-py2.py3-none-any.whl", hash = "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da"},
|
||||
{file = "pytest-cov-2.12.0.tar.gz", hash = "sha256:8535764137fecce504a49c2b742288e3d34bc09eed298ad65963616cc98fd45e"},
|
||||
{file = "pytest_cov-2.12.0-py2.py3-none-any.whl", hash = "sha256:95d4933dcbbacfa377bb60b29801daa30d90c33981ab2a79e9ab4452c165066e"},
|
||||
]
|
||||
pytest-html = [
|
||||
{file = "pytest-html-3.1.1.tar.gz", hash = "sha256:3ee1cf319c913d19fe53aeb0bc400e7b0bc2dbeb477553733db1dad12eb75ee3"},
|
||||
|
|
@ -1660,8 +1649,8 @@ pytest-metadata = [
|
|||
{file = "pytest_metadata-1.11.0-py2.py3-none-any.whl", hash = "sha256:576055b8336dd4a9006dd2a47615f76f2f8c30ab12b1b1c039d99e834583523f"},
|
||||
]
|
||||
pytest-mock = [
|
||||
{file = "pytest-mock-3.6.0.tar.gz", hash = "sha256:f7c3d42d6287f4e45846c8231c31902b6fa2bea98735af413a43da4cf5b727f1"},
|
||||
{file = "pytest_mock-3.6.0-py3-none-any.whl", hash = "sha256:952139a535b5b48ac0bb2f90b5dd36b67c7e1ba92601f3a8012678c4bd7f0bcc"},
|
||||
{file = "pytest-mock-3.6.1.tar.gz", hash = "sha256:40217a058c52a63f1042f0784f62009e976ba824c418cced42e88d5f40ab0e62"},
|
||||
{file = "pytest_mock-3.6.1-py3-none-any.whl", hash = "sha256:30c2f2cc9759e76eee674b81ea28c9f0b94f8f0445a1b87762cadf774f0df7e3"},
|
||||
]
|
||||
pytest-mypy = [
|
||||
{file = "pytest-mypy-0.8.1.tar.gz", hash = "sha256:1fa55723a4bf1d054fcba1c3bd694215a2a65cc95ab10164f5808afd893f3b11"},
|
||||
|
|
@ -1754,8 +1743,8 @@ requests = [
|
|||
{file = "requests-2.15.1.tar.gz", hash = "sha256:e5659b9315a0610505e050bb7190bf6fa2ccee1ac295f2b760ef9d8a03ebbb2e"},
|
||||
]
|
||||
six = [
|
||||
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
|
||||
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
]
|
||||
snowballstemmer = [
|
||||
{file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"},
|
||||
|
|
@ -1798,8 +1787,8 @@ sphinxcontrib-devhelp = [
|
|||
{file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"},
|
||||
]
|
||||
sphinxcontrib-htmlhelp = [
|
||||
{file = "sphinxcontrib-htmlhelp-1.0.3.tar.gz", hash = "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"},
|
||||
{file = "sphinxcontrib_htmlhelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f"},
|
||||
{file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"},
|
||||
{file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"},
|
||||
]
|
||||
sphinxcontrib-jsmath = [
|
||||
{file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"},
|
||||
|
|
@ -1810,8 +1799,8 @@ sphinxcontrib-qthelp = [
|
|||
{file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"},
|
||||
]
|
||||
sphinxcontrib-serializinghtml = [
|
||||
{file = "sphinxcontrib-serializinghtml-1.1.4.tar.gz", hash = "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc"},
|
||||
{file = "sphinxcontrib_serializinghtml-1.1.4-py2.py3-none-any.whl", hash = "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"},
|
||||
{file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"},
|
||||
{file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"},
|
||||
]
|
||||
toml = [
|
||||
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
||||
|
|
@ -1897,9 +1886,9 @@ typed-ast = [
|
|||
{file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
|
||||
]
|
||||
typing-extensions = [
|
||||
{file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
|
||||
{file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
|
||||
{file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
|
||||
{file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"},
|
||||
{file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"},
|
||||
{file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"},
|
||||
]
|
||||
uvloop = [
|
||||
{file = "uvloop-0.15.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:19fa1d56c91341318ac5d417e7b61c56e9a41183946cc70c411341173de02c69"},
|
||||
|
|
@ -1914,8 +1903,8 @@ uvloop = [
|
|||
{file = "uvloop-0.15.2.tar.gz", hash = "sha256:2bb0624a8a70834e54dde8feed62ed63b50bad7a1265c40d6403a2ac447bce01"},
|
||||
]
|
||||
virtualenv = [
|
||||
{file = "virtualenv-20.4.3-py2.py3-none-any.whl", hash = "sha256:83f95875d382c7abafe06bd2a4cdd1b363e1bb77e02f155ebe8ac082a916b37c"},
|
||||
{file = "virtualenv-20.4.3.tar.gz", hash = "sha256:49ec4eb4c224c6f7dd81bb6d0a28a09ecae5894f4e593c89b0db0885f565a107"},
|
||||
{file = "virtualenv-20.4.7-py2.py3-none-any.whl", hash = "sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76"},
|
||||
{file = "virtualenv-20.4.7.tar.gz", hash = "sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467"},
|
||||
]
|
||||
wcwidth = [
|
||||
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ classifiers = [
|
|||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: System Administrators",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
||||
|
|
@ -32,7 +31,7 @@ classifiers = [
|
|||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.7"
|
||||
python = "^3.8"
|
||||
aiohttp = "^3.7.4"
|
||||
pydantic = "^1.8.1"
|
||||
Babel = "^2.9.1"
|
||||
|
|
|
|||
|
|
@ -4,17 +4,17 @@ from typing import TYPE_CHECKING, AsyncGenerator, Deque, Optional, Type
|
|||
from aiogram import Bot
|
||||
from aiogram.client.session.base import BaseSession
|
||||
from aiogram.methods import TelegramMethod
|
||||
from aiogram.methods.base import Request, Response, T
|
||||
from aiogram.types import UNSET
|
||||
from aiogram.methods.base import Request, Response, TelegramType
|
||||
from aiogram.types import UNSET, ResponseParameters
|
||||
|
||||
|
||||
class MockedSession(BaseSession):
|
||||
def __init__(self):
|
||||
super(MockedSession, self).__init__()
|
||||
self.responses: Deque[Response[T]] = deque()
|
||||
self.responses: Deque[Response[TelegramType]] = deque()
|
||||
self.requests: Deque[Request] = deque()
|
||||
|
||||
def add_result(self, response: Response[T]) -> Response[T]:
|
||||
def add_result(self, response: Response[TelegramType]) -> Response[TelegramType]:
|
||||
self.responses.append(response)
|
||||
return response
|
||||
|
||||
|
|
@ -25,11 +25,13 @@ class MockedSession(BaseSession):
|
|||
pass
|
||||
|
||||
async def make_request(
|
||||
self, bot: Bot, method: TelegramMethod[T], timeout: Optional[int] = UNSET
|
||||
) -> T:
|
||||
self, bot: Bot, method: TelegramMethod[TelegramType], timeout: Optional[int] = UNSET
|
||||
) -> TelegramType:
|
||||
self.requests.append(method.build_request(bot))
|
||||
response: Response[T] = self.responses.pop()
|
||||
self.raise_for_status(response)
|
||||
response: Response[TelegramType] = self.responses.pop()
|
||||
self.check_response(
|
||||
method=method, status_code=response.error_code, content=response.json()
|
||||
)
|
||||
return response.result # type: ignore
|
||||
|
||||
async def stream_content(
|
||||
|
|
@ -47,21 +49,23 @@ class MockedBot(Bot):
|
|||
|
||||
def add_result_for(
|
||||
self,
|
||||
method: Type[TelegramMethod[T]],
|
||||
method: Type[TelegramMethod[TelegramType]],
|
||||
ok: bool,
|
||||
result: T = None,
|
||||
result: TelegramType = None,
|
||||
description: Optional[str] = None,
|
||||
error_code: Optional[int] = None,
|
||||
migrate_to_chat_id: Optional[int] = None,
|
||||
retry_after: Optional[int] = None,
|
||||
) -> Response[T]:
|
||||
) -> Response[TelegramType]:
|
||||
response = Response[method.__returning__]( # type: ignore
|
||||
ok=ok,
|
||||
result=result,
|
||||
description=description,
|
||||
error_code=error_code,
|
||||
migrate_to_chat_id=migrate_to_chat_id,
|
||||
retry_after=retry_after,
|
||||
parameters=ResponseParameters(
|
||||
migrate_to_chat_id=migrate_to_chat_id,
|
||||
retry_after=retry_after,
|
||||
),
|
||||
)
|
||||
self.session.add_result(response)
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -172,14 +172,10 @@ class TestAiohttpSession:
|
|||
return Request(method="method", data={})
|
||||
|
||||
call = TestMethod()
|
||||
with patch(
|
||||
"aiogram.client.session.base.BaseSession.raise_for_status"
|
||||
) as patched_raise_for_status:
|
||||
result = await session.make_request(bot, call)
|
||||
assert isinstance(result, int)
|
||||
assert result == 42
|
||||
|
||||
assert patched_raise_for_status.called_once()
|
||||
result = await session.make_request(bot, call)
|
||||
assert isinstance(result, int)
|
||||
assert result == 42
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_content(self, aresponses: ResponsesMockServer):
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ from typing import AsyncContextManager, AsyncGenerator, Optional
|
|||
|
||||
import pytest
|
||||
|
||||
from aiogram.client.session.base import BaseSession, T
|
||||
from aiogram.client.session.base import BaseSession, TelegramType
|
||||
from aiogram.client.telegram import PRODUCTION, TelegramAPIServer
|
||||
from aiogram.methods import GetMe, Response, TelegramMethod
|
||||
from aiogram.methods import DeleteMessage, GetMe, Response, TelegramMethod
|
||||
from aiogram.types import UNSET
|
||||
|
||||
try:
|
||||
|
|
@ -20,7 +20,7 @@ class CustomSession(BaseSession):
|
|||
async def close(self):
|
||||
pass
|
||||
|
||||
async def make_request(self, token: str, method: TelegramMethod[T], timeout: Optional[int] = UNSET) -> None: # type: ignore
|
||||
async def make_request(self, token: str, method: TelegramMethod[TelegramType], timeout: Optional[int] = UNSET) -> None: # type: ignore
|
||||
assert isinstance(token, str)
|
||||
assert isinstance(method, TelegramMethod)
|
||||
|
||||
|
|
@ -135,12 +135,20 @@ class TestBaseSession:
|
|||
|
||||
assert session.clean_json(42) == 42
|
||||
|
||||
def test_raise_for_status(self):
|
||||
def check_response(self):
|
||||
session = CustomSession()
|
||||
|
||||
session.raise_for_status(Response[bool](ok=True, result=True))
|
||||
session.check_response(
|
||||
method=DeleteMessage(chat_id=42, message_id=42),
|
||||
status_code=200,
|
||||
content='{"ok":true,"result":true}',
|
||||
)
|
||||
with pytest.raises(Exception):
|
||||
session.raise_for_status(Response[bool](ok=False, description="Error", error_code=400))
|
||||
session.check_response(
|
||||
method=DeleteMessage(chat_id=42, message_id=42),
|
||||
status_code=400,
|
||||
content='{"ok":false,"description":"test"}',
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_make_request(self):
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import pytest
|
|||
|
||||
from aiogram.methods import (
|
||||
CopyMessage,
|
||||
DeleteMessage,
|
||||
EditMessageCaption,
|
||||
EditMessageText,
|
||||
SendAnimation,
|
||||
SendAudio,
|
||||
SendContact,
|
||||
|
|
@ -549,3 +552,28 @@ class TestMessage:
|
|||
if method:
|
||||
assert isinstance(method, expected_method)
|
||||
# TODO: Check additional fields
|
||||
|
||||
def test_edit_text(self):
|
||||
message = Message(
|
||||
message_id=42, chat=Chat(id=42, type="private"), date=datetime.datetime.now()
|
||||
)
|
||||
method = message.edit_text(text="test")
|
||||
assert isinstance(method, EditMessageText)
|
||||
assert method.chat_id == message.chat.id
|
||||
|
||||
def test_edit_caption(self):
|
||||
message = Message(
|
||||
message_id=42, chat=Chat(id=42, type="private"), date=datetime.datetime.now()
|
||||
)
|
||||
method = message.edit_caption(caption="test")
|
||||
assert isinstance(method, EditMessageCaption)
|
||||
assert method.chat_id == message.chat.id
|
||||
|
||||
def test_delete(self):
|
||||
message = Message(
|
||||
message_id=42, chat=Chat(id=42, type="private"), date=datetime.datetime.now()
|
||||
)
|
||||
method = message.delete()
|
||||
assert isinstance(method, DeleteMessage)
|
||||
assert method.chat_id == message.chat.id
|
||||
assert method.message_id == message.message_id
|
||||
|
|
|
|||
177
tests/test_dispatcher/test_filters/test_callback_data.py
Normal file
177
tests/test_dispatcher/test_filters/test_callback_data.py
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
from decimal import Decimal
|
||||
from enum import Enum, auto
|
||||
from fractions import Fraction
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
from magic_filter import MagicFilter
|
||||
from pydantic import ValidationError
|
||||
|
||||
from aiogram import F
|
||||
from aiogram.dispatcher.filters.callback_data import CallbackData
|
||||
from aiogram.types import CallbackQuery, User
|
||||
|
||||
|
||||
class MyIntEnum(Enum):
|
||||
FOO = auto()
|
||||
|
||||
|
||||
class MyStringEnum(str, Enum):
|
||||
FOO = "FOO"
|
||||
|
||||
|
||||
class MyCallback(CallbackData, prefix="test"):
|
||||
foo: str
|
||||
bar: int
|
||||
|
||||
|
||||
class TestCallbackData:
|
||||
def test_init_subclass_prefix_required(self):
|
||||
assert MyCallback.prefix == "test"
|
||||
|
||||
with pytest.raises(ValueError, match="prefix required.+"):
|
||||
|
||||
class MyInvalidCallback(CallbackData):
|
||||
pass
|
||||
|
||||
def test_init_subclass_sep_validation(self):
|
||||
assert MyCallback.sep == ":"
|
||||
|
||||
class MyCallback2(CallbackData, prefix="test2", sep="@"):
|
||||
pass
|
||||
|
||||
assert MyCallback2.sep == "@"
|
||||
|
||||
with pytest.raises(ValueError, match="Separator symbol '@' .+ 'sp@m'"):
|
||||
|
||||
class MyInvalidCallback(CallbackData, prefix="sp@m", sep="@"):
|
||||
pass
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value,success,expected",
|
||||
[
|
||||
[None, True, ""],
|
||||
[42, True, "42"],
|
||||
["test", True, "test"],
|
||||
[9.99, True, "9.99"],
|
||||
[Decimal("9.99"), True, "9.99"],
|
||||
[Fraction("3/2"), True, "3/2"],
|
||||
[
|
||||
UUID("123e4567-e89b-12d3-a456-426655440000"),
|
||||
True,
|
||||
"123e4567-e89b-12d3-a456-426655440000",
|
||||
],
|
||||
[MyIntEnum.FOO, True, "1"],
|
||||
[MyStringEnum.FOO, True, "FOO"],
|
||||
[..., False, "..."],
|
||||
[object, False, "..."],
|
||||
[object(), False, "..."],
|
||||
[User(id=42, is_bot=False, first_name="test"), False, "..."],
|
||||
],
|
||||
)
|
||||
def test_encode_value(self, value, success, expected):
|
||||
callback = MyCallback(foo="test", bar=42)
|
||||
if success:
|
||||
assert callback._encode_value("test", value) == expected
|
||||
else:
|
||||
with pytest.raises(ValueError):
|
||||
assert callback._encode_value("test", value) == expected
|
||||
|
||||
def test_pack(self):
|
||||
with pytest.raises(ValueError, match="Separator symbol .+"):
|
||||
assert MyCallback(foo="te:st", bar=42).pack()
|
||||
|
||||
with pytest.raises(ValueError, match=".+is too long.+"):
|
||||
assert MyCallback(foo="test" * 32, bar=42).pack()
|
||||
|
||||
assert MyCallback(foo="test", bar=42).pack() == "test:test:42"
|
||||
|
||||
def test_pack_optional(self):
|
||||
class MyCallback1(CallbackData, prefix="test1"):
|
||||
foo: str
|
||||
bar: Optional[int] = None
|
||||
|
||||
assert MyCallback1(foo="spam").pack() == "test1:spam:"
|
||||
assert MyCallback1(foo="spam", bar=42).pack() == "test1:spam:42"
|
||||
|
||||
class MyCallback2(CallbackData, prefix="test2"):
|
||||
foo: Optional[str] = None
|
||||
bar: int
|
||||
|
||||
assert MyCallback2(bar=42).pack() == "test2::42"
|
||||
assert MyCallback2(foo="spam", bar=42).pack() == "test2:spam:42"
|
||||
|
||||
class MyCallback3(CallbackData, prefix="test3"):
|
||||
foo: Optional[str] = "experiment"
|
||||
bar: int
|
||||
|
||||
assert MyCallback3(bar=42).pack() == "test3:experiment:42"
|
||||
assert MyCallback3(foo="spam", bar=42).pack() == "test3:spam:42"
|
||||
|
||||
def test_unpack(self):
|
||||
with pytest.raises(TypeError, match=".+ takes 2 arguments but 3 were given"):
|
||||
MyCallback.unpack("test:test:test:test")
|
||||
|
||||
with pytest.raises(ValueError, match="Bad prefix .+"):
|
||||
MyCallback.unpack("spam:test:test")
|
||||
|
||||
assert MyCallback.unpack("test:test:42") == MyCallback(foo="test", bar=42)
|
||||
|
||||
def test_unpack_optional(self):
|
||||
with pytest.raises(ValidationError):
|
||||
assert MyCallback.unpack("test:test:")
|
||||
|
||||
class MyCallback1(CallbackData, prefix="test1"):
|
||||
foo: str
|
||||
bar: Optional[int] = None
|
||||
|
||||
assert MyCallback1.unpack("test1:spam:") == MyCallback1(foo="spam")
|
||||
assert MyCallback1.unpack("test1:spam:42") == MyCallback1(foo="spam", bar=42)
|
||||
|
||||
class MyCallback2(CallbackData, prefix="test2"):
|
||||
foo: Optional[str] = None
|
||||
bar: int
|
||||
|
||||
assert MyCallback2.unpack("test2::42") == MyCallback2(bar=42)
|
||||
assert MyCallback2.unpack("test2:spam:42") == MyCallback2(foo="spam", bar=42)
|
||||
|
||||
class MyCallback3(CallbackData, prefix="test3"):
|
||||
foo: Optional[str] = "experiment"
|
||||
bar: int
|
||||
|
||||
assert MyCallback3.unpack("test3:experiment:42") == MyCallback3(bar=42)
|
||||
assert MyCallback3.unpack("test3:spam:42") == MyCallback3(foo="spam", bar=42)
|
||||
|
||||
def test_build_filter(self):
|
||||
filter_object = MyCallback.filter(F.foo == "test")
|
||||
assert isinstance(filter_object.rule, MagicFilter)
|
||||
assert filter_object.callback_data is MyCallback
|
||||
|
||||
|
||||
class TestCallbackDataFilter:
|
||||
@pytest.mark.parametrize(
|
||||
"query,rule,result",
|
||||
[
|
||||
["test", F.foo == "test", False],
|
||||
["test:spam:42", F.foo == "test", False],
|
||||
["test:test:42", F.foo == "test", {"callback_data": MyCallback(foo="test", bar=42)}],
|
||||
["test:test:", F.foo == "test", False],
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_call(self, query, rule, result):
|
||||
callback_query = CallbackQuery(
|
||||
id="1",
|
||||
from_user=User(id=42, is_bot=False, first_name="test"),
|
||||
data=query,
|
||||
chat_instance="test",
|
||||
)
|
||||
|
||||
filter_object = MyCallback.filter(rule)
|
||||
assert await filter_object(callback_query) == result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalid_call(self):
|
||||
filter_object = MyCallback.filter(F.test)
|
||||
assert not await filter_object(User(id=42, is_bot=False, first_name="test"))
|
||||
Loading…
Add table
Add a link
Reference in a new issue