Skip to content

Commit

Permalink
[3.0] Bot API 5.1 + FSM + Utils (#525)
Browse files Browse the repository at this point in the history
* Regenerate corresponding to Bot API 5.1

* Added base of FSM. Markup constructor and small refactoring

* Fix dependencies

* Fix mypy windows error

* Move StatesGroup.get_root() from meta to class

* Fixed chat and user constraints

* Update pipeline

* Remove docs pipeline

* Added GLOBAL_USER FSM strategy

* Reformat code

* Fixed Dispatcher._process_update

* Bump Bot API 5.2. Added integration with MagicFilter

* Coverage
  • Loading branch information
JrooTJunior committed May 11, 2021
1 parent a6f824a commit 0e72d8e
Show file tree
Hide file tree
Showing 265 changed files with 2,921 additions and 1,324 deletions.
2 changes: 1 addition & 1 deletion .apiversion
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4.9
5.1
58 changes: 0 additions & 58 deletions .github/workflows/docs.yml

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:

- name: Install dependencies
run: |
python -m pip install --upgrade pip poetry==1.1.4
python -m pip install --upgrade pip poetry
poetry install
- name: Lint code
Expand Down
8 changes: 6 additions & 2 deletions aiogram/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from magic_filter import MagicFilter

from .client import session
from .client.bot import Bot
from .dispatcher import filters, handler
Expand All @@ -10,8 +12,9 @@

_uvloop.install()
except ImportError: # pragma: no cover
_uvloop = None
pass

F = MagicFilter()

__all__ = (
"__api_version__",
Expand All @@ -25,7 +28,8 @@
"BaseMiddleware",
"filters",
"handler",
"F",
)

__version__ = "3.0.0-alpha.6"
__api_version__ = "4.9"
__api_version__ = "5.1"
139 changes: 118 additions & 21 deletions aiogram/client/bot.py

Large diffs are not rendered by default.

49 changes: 41 additions & 8 deletions aiogram/dispatcher/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
from ..utils.exceptions import TelegramAPIError
from .event.bases import UNHANDLED, SkipHandler
from .event.telegram import TelegramEventObserver
from .fsm.context import FSMContext
from .fsm.middleware import FSMContextMiddleware
from .fsm.storage.base import BaseStorage
from .fsm.storage.memory import MemoryStorage
from .fsm.strategy import FSMStrategy
from .middlewares.error import ErrorsMiddleware
from .middlewares.user_context import UserContextMiddleware
from .router import Router
Expand All @@ -23,15 +28,36 @@ class Dispatcher(Router):
Root router
"""

def __init__(self, **kwargs: Any) -> None:
def __init__(
self,
storage: Optional[BaseStorage] = None,
fsm_strategy: FSMStrategy = FSMStrategy.USER_IN_CHAT,
isolate_events: bool = True,
**kwargs: Any,
) -> None:
super(Dispatcher, self).__init__(**kwargs)

self.update = TelegramEventObserver(router=self, event_name="update")
self.observers["update"] = self.update

# Telegram API provides originally only one event type - Update
# For making easily interactions with events here is registered handler which helps
# to separate Update to different event types like Message, CallbackQuery and etc.
self.update = self.observers["update"] = TelegramEventObserver(
router=self, event_name="update"
)
self.update.register(self._listen_update)
self.update.outer_middleware(UserContextMiddleware())

# Error handlers should works is out of all other functions and be registered before all other middlewares
self.update.outer_middleware(ErrorsMiddleware(self))
# User context middleware makes small optimization for all other builtin
# middlewares via caching the user and chat instances in the event context
self.update.outer_middleware(UserContextMiddleware())
# FSM middleware should always be registered after User context middleware
# because here is used context from previous step
self.fsm = FSMContextMiddleware(
storage=storage if storage else MemoryStorage(),
strategy=fsm_strategy,
isolate_events=isolate_events,
)
self.update.outer_middleware(self.fsm)

self._running_lock = Lock()

Expand Down Expand Up @@ -150,6 +176,12 @@ async def _listen_update(self, update: Update, **kwargs: Any) -> Any:
elif update.poll_answer:
update_type = "poll_answer"
event = update.poll_answer
elif update.my_chat_member:
update_type = "my_chat_member"
event = update.my_chat_member
elif update.chat_member:
update_type = "chat_member"
event = update.chat_member
else:
warnings.warn(
"Detected unknown update type.\n"
Expand Down Expand Up @@ -201,13 +233,11 @@ async def _process_update(
:param kwargs: contextual data for middlewares, filters and handlers
:return: status
"""
handled = False
try:
response = await self.feed_update(bot, update, **kwargs)
handled = handled is not UNHANDLED
if call_answer and isinstance(response, TelegramMethod):
await self._silent_call_request(bot=bot, result=response)
return handled
return response is not UNHANDLED

except Exception as e:
loggers.dispatcher.exception(
Expand Down Expand Up @@ -347,3 +377,6 @@ def run_polling(self, *bots: Bot, **kwargs: Any) -> None:
except (KeyboardInterrupt, SystemExit): # pragma: no cover
# Allow to graceful shutdown
pass

def current_state(self, user_id: int, chat_id: int) -> FSMContext:
return self.fsm.get_context(user_id=user_id, chat_id=chat_id)
12 changes: 11 additions & 1 deletion aiogram/dispatcher/event/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
from functools import partial
from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Type, Union

from magic_filter import MagicFilter

from aiogram.dispatcher.filters.base import BaseFilter
from aiogram.dispatcher.handler.base import BaseHandler

CallbackType = Callable[..., Awaitable[Any]]
SyncFilter = Callable[..., Any]
AsyncFilter = Callable[..., Awaitable[Any]]
FilterType = Union[SyncFilter, AsyncFilter, BaseFilter]
FilterType = Union[SyncFilter, AsyncFilter, BaseFilter, MagicFilter]
HandlerType = Union[FilterType, Type[BaseHandler]]


Expand Down Expand Up @@ -47,6 +49,14 @@ async def call(self, *args: Any, **kwargs: Any) -> Any:
class FilterObject(CallableMixin):
callback: FilterType

def __post_init__(self) -> None:
# TODO: Make possibility to extract and explain magic from filter object.
# Current solution is hard for debugging because the MagicFilter instance can't be extracted
if isinstance(self.callback, MagicFilter):
# MagicFilter instance is callable but generates only "CallOperation" instead of applying the filter
self.callback = self.callback.resolve
super().__post_init__()


@dataclass
class HandlerObject(CallableMixin):
Expand Down
2 changes: 2 additions & 0 deletions aiogram/dispatcher/filters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,7 @@
"pre_checkout_query": (),
"poll": (),
"poll_answer": (),
"my_chat_member": (),
"chat_member": (),
"error": (ExceptionMessageFilter, ExceptionTypeFilter),
}
2 changes: 1 addition & 1 deletion aiogram/dispatcher/filters/content_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def _validate_content_types(
cls, value: Optional[Union[Sequence[str], str]]
) -> Optional[Sequence[str]]:
if not value:
value = [ContentType.TEXT]
return value
if isinstance(value, str):
value = [value]
allowed_content_types = set(ContentType.all())
Expand Down
Empty file.
35 changes: 35 additions & 0 deletions aiogram/dispatcher/fsm/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from typing import Any, Dict, Optional

from aiogram.dispatcher.fsm.storage.base import BaseStorage, StateType


class FSMContext:
def __init__(self, storage: BaseStorage, chat_id: int, user_id: int) -> None:
self.storage = storage
self.chat_id = chat_id
self.user_id = user_id

async def set_state(self, state: StateType = None) -> None:
await self.storage.set_state(chat_id=self.chat_id, user_id=self.user_id, state=state)

async def get_state(self) -> Optional[str]:
return await self.storage.get_state(chat_id=self.chat_id, user_id=self.user_id)

async def set_data(self, data: Dict[str, Any]) -> None:
await self.storage.set_data(chat_id=self.chat_id, user_id=self.user_id, data=data)

async def get_data(self) -> Dict[str, Any]:
return await self.storage.get_data(chat_id=self.chat_id, user_id=self.user_id)

async def update_data(
self, data: Optional[Dict[str, Any]] = None, **kwargs: Any
) -> Dict[str, Any]:
if data:
kwargs.update(data)
return await self.storage.update_data(
chat_id=self.chat_id, user_id=self.user_id, data=kwargs
)

async def clear(self) -> None:
await self.set_state(state=None)
await self.set_data({})
53 changes: 53 additions & 0 deletions aiogram/dispatcher/fsm/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from typing import Any, Awaitable, Callable, Dict, Optional

from aiogram.dispatcher.fsm.context import FSMContext
from aiogram.dispatcher.fsm.storage.base import BaseStorage
from aiogram.dispatcher.fsm.strategy import FSMStrategy, apply_strategy
from aiogram.dispatcher.middlewares.base import BaseMiddleware
from aiogram.types import Update


class FSMContextMiddleware(BaseMiddleware[Update]):
def __init__(
self,
storage: BaseStorage,
strategy: FSMStrategy = FSMStrategy.USER_IN_CHAT,
isolate_events: bool = True,
) -> None:
self.storage = storage
self.strategy = strategy
self.isolate_events = isolate_events

async def __call__(
self,
handler: Callable[[Update, Dict[str, Any]], Awaitable[Any]],
event: Update,
data: Dict[str, Any],
) -> Any:
context = self._resolve_context(data)
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)
return await handler(event, data)

def _resolve_context(self, data: Dict[str, Any]) -> Optional[FSMContext]:
user = data.get("event_from_user")
chat = data.get("event_chat")
chat_id = chat.id if chat else None
user_id = user.id if user else None

if chat_id is None:
chat_id = user_id

if chat_id is not None and user_id is not None:
chat_id, user_id = apply_strategy(
chat_id=chat_id, user_id=user_id, strategy=self.strategy
)
return self.get_context(chat_id=chat_id, user_id=user_id)
return None

def get_context(self, chat_id: int, user_id: int) -> FSMContext:
return FSMContext(storage=self.storage, chat_id=chat_id, user_id=user_id)

0 comments on commit 0e72d8e

Please sign in to comment.