aiogram-tool 1.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aiogram_tool/__init__.py +0 -0
- aiogram_tool/storage/__init__.py +15 -0
- aiogram_tool/storage/base.py +20 -0
- aiogram_tool/storage/impl/__init__.py +0 -0
- aiogram_tool/storage/impl/file.py +72 -0
- aiogram_tool/storage/impl/memory.py +33 -0
- aiogram_tool/storage/impl/redis.py +39 -0
- aiogram_tool/tools/__init__.py +0 -0
- aiogram_tool/tools/callback_data/__init__.py +4 -0
- aiogram_tool/tools/callback_data/answer.py +10 -0
- aiogram_tool/tools/callback_data/filter.py +120 -0
- aiogram_tool/tools/depend/__init__.py +16 -0
- aiogram_tool/tools/depend/components/__init__.py +0 -0
- aiogram_tool/tools/depend/components/exit.py +4 -0
- aiogram_tool/tools/depend/components/filter.py +63 -0
- aiogram_tool/tools/depend/components/inner_middleware.py +54 -0
- aiogram_tool/tools/depend/components/outer_middleware.py +27 -0
- aiogram_tool/tools/depend/depend.py +20 -0
- aiogram_tool/tools/depend/storage/__init__.py +0 -0
- aiogram_tool/tools/depend/storage/memory.py +29 -0
- aiogram_tool/tools/depend/tool.py +72 -0
- aiogram_tool/tools/depend/types/__init__.py +0 -0
- aiogram_tool/tools/depend/types/enums.py +15 -0
- aiogram_tool/tools/depend/types/exceptions.py +25 -0
- aiogram_tool/tools/depend/types/schema.py +19 -0
- aiogram_tool/tools/depend/utils/__init__.py +0 -0
- aiogram_tool/tools/depend/utils/inspect.py +56 -0
- aiogram_tool/tools/depend/utils/registry_manager.py +64 -0
- aiogram_tool/tools/depend/utils/resolver.py +163 -0
- aiogram_tool/tools/depend/utils/scope_registry.py +42 -0
- aiogram_tool/tools/depend/utils/stack_manager.py +35 -0
- aiogram_tool/tools/limit/__init__.py +5 -0
- aiogram_tool/tools/limit/answer.py +20 -0
- aiogram_tool/tools/limit/filter.py +82 -0
- aiogram_tool/tools/limit/rate_limit/__init__.py +9 -0
- aiogram_tool/tools/limit/rate_limit/base.py +36 -0
- aiogram_tool/tools/limit/rate_limit/fixed_window.py +70 -0
- aiogram_tool/tools/limit/rate_limit/sliding_window.py +79 -0
- aiogram_tool/tools/limit/rate_limit/token_bucket.py +102 -0
- aiogram_tool/tools/limit/schema.py +24 -0
- aiogram_tool/tools/limit/tool.py +21 -0
- aiogram_tool/tools/setup.py +25 -0
- aiogram_tool/types.py +1 -0
- aiogram_tool-1.2.2.dist-info/METADATA +235 -0
- aiogram_tool-1.2.2.dist-info/RECORD +47 -0
- aiogram_tool-1.2.2.dist-info/WHEEL +4 -0
- aiogram_tool-1.2.2.dist-info/licenses/LICENSE.md +21 -0
aiogram_tool/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .impl.file import FileLockStorage, FileStorage
|
|
2
|
+
from .impl.memory import (
|
|
3
|
+
MemoryLockStorage,
|
|
4
|
+
MemoryStorage,
|
|
5
|
+
)
|
|
6
|
+
from .impl.redis import AsyncRedisLockStorage, AsyncRedisStorage
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"FileStorage",
|
|
10
|
+
"MemoryStorage",
|
|
11
|
+
"AsyncRedisStorage",
|
|
12
|
+
"MemoryLockStorage",
|
|
13
|
+
"FileLockStorage",
|
|
14
|
+
"AsyncRedisLockStorage",
|
|
15
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from contextlib import AbstractAsyncContextManager
|
|
3
|
+
|
|
4
|
+
from aiogram_tool.types import _MISSING
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseStorage(ABC):
|
|
8
|
+
@abstractmethod
|
|
9
|
+
async def set_value(self, key: str, value: str) -> None:
|
|
10
|
+
raise NotImplementedError
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
async def get_value(self, key: str) -> str | _MISSING:
|
|
14
|
+
raise NotImplementedError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BaseLockStorage(BaseStorage):
|
|
18
|
+
@abstractmethod
|
|
19
|
+
async def lock(self, key: str) -> AbstractAsyncContextManager[None]:
|
|
20
|
+
raise NotImplementedError
|
|
File without changes
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from asyncio import Lock
|
|
3
|
+
from collections.abc import MutableMapping
|
|
4
|
+
|
|
5
|
+
import aiofiles
|
|
6
|
+
|
|
7
|
+
from aiogram_tool.storage.base import BaseLockStorage
|
|
8
|
+
from aiogram_tool.storage.impl.memory import MemoryStorage
|
|
9
|
+
from aiogram_tool.types import _MISSING
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FileStorage(MemoryStorage):
|
|
13
|
+
def __init__(self, file: str, storage: MutableMapping | None = None) -> None:
|
|
14
|
+
if not os.path.exists(file):
|
|
15
|
+
raise FileNotFoundError(f"File {file} not found")
|
|
16
|
+
|
|
17
|
+
self.file = file
|
|
18
|
+
|
|
19
|
+
self._is_memory = False
|
|
20
|
+
if storage is not None:
|
|
21
|
+
self._is_memory = True
|
|
22
|
+
super().__init__(storage=storage)
|
|
23
|
+
|
|
24
|
+
async def set_value(self, key: str, value: str) -> None:
|
|
25
|
+
if "&" in key:
|
|
26
|
+
raise ValueError(f"Symbol & can't use in key {key}")
|
|
27
|
+
|
|
28
|
+
async with aiofiles.open(self.file, "a") as aiofile:
|
|
29
|
+
await aiofile.write(f"\n{key}&{value}")
|
|
30
|
+
|
|
31
|
+
if self._is_memory:
|
|
32
|
+
await super().set_value(key=key, value=value)
|
|
33
|
+
|
|
34
|
+
async def get_value(self, key: str) -> str | _MISSING:
|
|
35
|
+
if self._is_memory:
|
|
36
|
+
value = await super().get_value(key=key)
|
|
37
|
+
if value:
|
|
38
|
+
return value
|
|
39
|
+
|
|
40
|
+
async with aiofiles.open(self.file) as aiofile:
|
|
41
|
+
data = await aiofile.readlines()
|
|
42
|
+
data.reverse()
|
|
43
|
+
|
|
44
|
+
for line in data:
|
|
45
|
+
line = line.strip()
|
|
46
|
+
if not line:
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
line_key, line_value = line.split(sep="&", maxsplit=1)
|
|
50
|
+
if line_key == key:
|
|
51
|
+
if self._is_memory:
|
|
52
|
+
await super().set_value(key, line_value)
|
|
53
|
+
return line_value
|
|
54
|
+
return _MISSING
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class FileLockStorage(FileStorage, BaseLockStorage):
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
file: str,
|
|
61
|
+
storage: MutableMapping | None = None,
|
|
62
|
+
locks_storage: MutableMapping | None = None,
|
|
63
|
+
) -> None:
|
|
64
|
+
self.global_lock = Lock()
|
|
65
|
+
self.locks = locks_storage if locks_storage is not None else {}
|
|
66
|
+
super().__init__(file=file, storage=storage)
|
|
67
|
+
|
|
68
|
+
async def lock(self, key: str) -> Lock:
|
|
69
|
+
async with self.global_lock:
|
|
70
|
+
if key not in self.locks.keys():
|
|
71
|
+
self.locks[key] = Lock()
|
|
72
|
+
return self.locks[key]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from asyncio import Lock
|
|
2
|
+
from collections.abc import MutableMapping
|
|
3
|
+
|
|
4
|
+
from aiogram_tool.storage.base import BaseLockStorage, BaseStorage
|
|
5
|
+
from aiogram_tool.types import _MISSING
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MemoryStorage(BaseStorage):
|
|
9
|
+
def __init__(self, storage: MutableMapping | None = None) -> None:
|
|
10
|
+
self.storage = storage if storage is not None else {}
|
|
11
|
+
|
|
12
|
+
async def set_value(self, key: str, value: str) -> None:
|
|
13
|
+
self.storage[key] = value
|
|
14
|
+
|
|
15
|
+
async def get_value(self, key: str) -> str | _MISSING:
|
|
16
|
+
return self.storage.get(key, _MISSING)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MemoryLockStorage(MemoryStorage, BaseLockStorage):
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
storage: MutableMapping | None = None,
|
|
23
|
+
locks_storage: MutableMapping | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
self.global_lock = Lock()
|
|
26
|
+
self.locks = locks_storage if locks_storage is not None else {}
|
|
27
|
+
super().__init__(storage=storage)
|
|
28
|
+
|
|
29
|
+
async def lock(self, key: str) -> Lock:
|
|
30
|
+
async with self.global_lock:
|
|
31
|
+
if key not in self.locks.keys():
|
|
32
|
+
self.locks[key] = Lock()
|
|
33
|
+
return self.locks[key]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from redis.asyncio import Redis as AsyncRedis
|
|
4
|
+
from redis.asyncio.lock import Lock as RedisLock
|
|
5
|
+
|
|
6
|
+
from aiogram_tool.storage.base import BaseLockStorage, BaseStorage
|
|
7
|
+
from aiogram_tool.types import _MISSING
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AsyncRedisStorage(BaseStorage):
|
|
11
|
+
def __init__(self, redis: AsyncRedis, expire: int | None = None) -> None:
|
|
12
|
+
self.redis = redis
|
|
13
|
+
self.expire = expire
|
|
14
|
+
|
|
15
|
+
async def get_value(self, key: str) -> str | _MISSING:
|
|
16
|
+
value = await self.redis.get(name=key)
|
|
17
|
+
if value is not None:
|
|
18
|
+
return value.decode() if isinstance(value, bytes) else value
|
|
19
|
+
return _MISSING
|
|
20
|
+
|
|
21
|
+
async def set_value(self, key: str, value: Any) -> None:
|
|
22
|
+
await self.redis.set(name=key, value=value, ex=self.expire)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AsyncRedisLockStorage(AsyncRedisStorage, BaseLockStorage):
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
redis: AsyncRedis,
|
|
29
|
+
expire: int | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
super().__init__(redis=redis, expire=expire)
|
|
32
|
+
|
|
33
|
+
async def lock(self, key: str) -> RedisLock:
|
|
34
|
+
lock_key = f"aigram_tool_lock:{key}"
|
|
35
|
+
return self.redis.lock(
|
|
36
|
+
name=lock_key,
|
|
37
|
+
timeout=10,
|
|
38
|
+
blocking_timeout=10,
|
|
39
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from aiogram.types import CallbackQuery
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CallbackDataAnswer:
|
|
7
|
+
"""Called when callback data for the button is not found."""
|
|
8
|
+
|
|
9
|
+
async def __call__(self, query: CallbackQuery) -> Any:
|
|
10
|
+
await query.answer(text="Button expired", show_alert=True)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import secrets
|
|
2
|
+
from typing import Any, ClassVar, Self
|
|
3
|
+
|
|
4
|
+
from aiogram.filters.callback_data import CallbackData, CallbackQueryFilter
|
|
5
|
+
from aiogram.types import CallbackQuery
|
|
6
|
+
from magic_filter import MagicFilter
|
|
7
|
+
|
|
8
|
+
from aiogram_tool.storage.base import BaseStorage
|
|
9
|
+
from aiogram_tool.storage.impl.memory import MemoryStorage
|
|
10
|
+
from aiogram_tool.tools.callback_data.answer import CallbackDataAnswer
|
|
11
|
+
from aiogram_tool.types import _MISSING
|
|
12
|
+
|
|
13
|
+
UNIQUE_PREFIX: str = "UIDPR"
|
|
14
|
+
STORAGE_PREFIX: str = "aiot_callback_data"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _UniqueIDCallbackData(CallbackData, prefix=UNIQUE_PREFIX):
|
|
18
|
+
unique_id: str
|
|
19
|
+
callback_data_prefix: str
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def build(cls, callback_data: type[CallbackData]) -> Self:
|
|
23
|
+
separators_len = len(cls.__separator__) * len(cls.model_fields)
|
|
24
|
+
unique_id_len = (
|
|
25
|
+
64 - (len(UNIQUE_PREFIX) + len(callback_data.__prefix__) + separators_len)
|
|
26
|
+
) // 2
|
|
27
|
+
|
|
28
|
+
if unique_id_len < 6:
|
|
29
|
+
raise ValueError(
|
|
30
|
+
f"Prefix '{callback_data.__prefix__}' at {callback_data.__name__} is too long. "
|
|
31
|
+
f"Unique id must be at least 6 bytes (12 chars)."
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
return cls(
|
|
35
|
+
unique_id=secrets.token_hex(unique_id_len),
|
|
36
|
+
callback_data_prefix=callback_data.__prefix__,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def get_storage_key(self) -> str:
|
|
40
|
+
return f"{STORAGE_PREFIX}@{self.unique_id}"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class LongCallbackQueryFilter(CallbackQueryFilter):
|
|
44
|
+
async def __call__(self, query: CallbackQuery) -> bool | dict[str, Any]:
|
|
45
|
+
if not isinstance(query, CallbackQuery) or not query.data:
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
instance = _UniqueIDCallbackData.unpack(query.data)
|
|
50
|
+
except (TypeError, ValueError):
|
|
51
|
+
try:
|
|
52
|
+
instance = self.callback_data.unpack(query.data)
|
|
53
|
+
except (TypeError, ValueError):
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
if isinstance(instance, _UniqueIDCallbackData):
|
|
57
|
+
if instance.callback_data_prefix != self.callback_data.__prefix__:
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
storage: BaseStorage = self.callback_data._storage
|
|
61
|
+
answer_callback: CallbackDataAnswer = self.callback_data._answer_callback
|
|
62
|
+
|
|
63
|
+
packed_callback_data = await storage.get_value(
|
|
64
|
+
key=instance.get_storage_key()
|
|
65
|
+
)
|
|
66
|
+
if packed_callback_data is _MISSING:
|
|
67
|
+
await answer_callback(query)
|
|
68
|
+
return False
|
|
69
|
+
try:
|
|
70
|
+
instance = self.callback_data.unpack(packed_callback_data)
|
|
71
|
+
except (TypeError, ValueError):
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
if self.rule is None or self.rule.resolve(instance):
|
|
75
|
+
return {"callback_data": instance}
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class LongCallbackData(CallbackData, prefix="?"):
|
|
80
|
+
"""Class registering long callback data
|
|
81
|
+
|
|
82
|
+
_storage - storage for long values\n
|
|
83
|
+
_answer_callback - A callback that triggers if the callback data for the button is not found in the storage.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
_storage: ClassVar[BaseStorage] = MemoryStorage()
|
|
87
|
+
_answer_callback: ClassVar[CallbackDataAnswer] = CallbackDataAnswer()
|
|
88
|
+
|
|
89
|
+
def _pack_without_errors(self) -> str:
|
|
90
|
+
result = [self.__prefix__]
|
|
91
|
+
for key, value in self.model_dump(mode="python").items():
|
|
92
|
+
encoded = self._encode_value(key, value)
|
|
93
|
+
result.append(encoded)
|
|
94
|
+
return self.__separator__.join(result)
|
|
95
|
+
|
|
96
|
+
async def pack_long(self) -> str:
|
|
97
|
+
"""A method that catches the long-data error
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
str: A unique ID generated if a "long data" error was caught
|
|
101
|
+
or resulting from the call to the parent `pack` method.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
return super().pack()
|
|
106
|
+
except ValueError as ex:
|
|
107
|
+
if "data is too long!" in str(ex):
|
|
108
|
+
callback_data_instance = _UniqueIDCallbackData.build(
|
|
109
|
+
callback_data=self.__class__
|
|
110
|
+
)
|
|
111
|
+
await self._storage.set_value(
|
|
112
|
+
key=callback_data_instance.get_storage_key(),
|
|
113
|
+
value=self._pack_without_errors(),
|
|
114
|
+
)
|
|
115
|
+
return callback_data_instance.pack()
|
|
116
|
+
raise ex
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def filter(cls, rule: MagicFilter | None = None) -> LongCallbackQueryFilter:
|
|
120
|
+
return LongCallbackQueryFilter(callback_data=cls, rule=rule)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .components.exit import DependExit
|
|
2
|
+
from .components.filter import DependFilter
|
|
3
|
+
from .depend import Depends, From
|
|
4
|
+
from .tool import DependTool
|
|
5
|
+
from .types.enums import Scope
|
|
6
|
+
from .utils.scope_registry import ScopeRegistry
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Depends",
|
|
10
|
+
"From",
|
|
11
|
+
"DependExit",
|
|
12
|
+
"DependFilter",
|
|
13
|
+
"ScopeRegistry",
|
|
14
|
+
"Scope",
|
|
15
|
+
"DependTool",
|
|
16
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from aiogram import Dispatcher
|
|
4
|
+
from aiogram.filters import Filter
|
|
5
|
+
|
|
6
|
+
from aiogram_tool.tools.depend.depend import From
|
|
7
|
+
from aiogram_tool.tools.depend.tool import DependTool
|
|
8
|
+
from aiogram_tool.tools.depend.types.exceptions import (
|
|
9
|
+
InvalidDependencyError,
|
|
10
|
+
NotFoundDependTool,
|
|
11
|
+
)
|
|
12
|
+
from aiogram_tool.tools.depend.utils.registry_manager import DependRegistryTransaction
|
|
13
|
+
from aiogram_tool.tools.depend.utils.resolver import DependResolver
|
|
14
|
+
from aiogram_tool.tools.depend.utils.stack_manager import AsyncExitStackTransaction
|
|
15
|
+
|
|
16
|
+
from .exit import DependExit
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DependFilter(Filter):
|
|
20
|
+
"""This class allows invoking a dependency at the filter level."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, *dependencies: From) -> None:
|
|
23
|
+
for dep in dependencies:
|
|
24
|
+
if not isinstance(dep, From):
|
|
25
|
+
raise InvalidDependencyError(f"Invalid type in dependency {dep}")
|
|
26
|
+
|
|
27
|
+
self._dependencies = dependencies
|
|
28
|
+
|
|
29
|
+
def get_depend_tool(self, data: dict[str, Any]) -> DependTool:
|
|
30
|
+
dispatcher: Dispatcher = data.get("dispatcher")
|
|
31
|
+
depend_tool = dispatcher.workflow_data.get("depend_tool")
|
|
32
|
+
|
|
33
|
+
if not isinstance(depend_tool, DependTool):
|
|
34
|
+
raise NotFoundDependTool("Not found DependTool. Add it in setup")
|
|
35
|
+
return depend_tool
|
|
36
|
+
|
|
37
|
+
def get_transactions(
|
|
38
|
+
self, data: dict[str, Any]
|
|
39
|
+
) -> tuple[DependRegistryTransaction, AsyncExitStackTransaction]:
|
|
40
|
+
return data.get("request_registry"), data.get("request_stack")
|
|
41
|
+
|
|
42
|
+
async def __call__(self, *args, **kwargs) -> bool:
|
|
43
|
+
depend_tool = self.get_depend_tool(kwargs)
|
|
44
|
+
req_registry, req_stack = self.get_transactions(kwargs)
|
|
45
|
+
|
|
46
|
+
resolver = DependResolver(
|
|
47
|
+
handler_callback=lambda: 1,
|
|
48
|
+
registry=req_registry,
|
|
49
|
+
stack=req_stack,
|
|
50
|
+
scope_registry=depend_tool.scope_registry,
|
|
51
|
+
middleware_data=kwargs.copy(),
|
|
52
|
+
dependency_override=depend_tool.dependency_override,
|
|
53
|
+
)
|
|
54
|
+
for depend in self._dependencies:
|
|
55
|
+
scope_object = depend_tool.scope_registry.get_scope_object(depend)
|
|
56
|
+
try:
|
|
57
|
+
params = await resolver.resolve_depend_params(
|
|
58
|
+
scope_object=scope_object, resolved_depends=set()
|
|
59
|
+
)
|
|
60
|
+
await resolver.depend_call(scope_object=scope_object, params=params)
|
|
61
|
+
except DependExit:
|
|
62
|
+
return False
|
|
63
|
+
return True
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from collections.abc import Awaitable, Callable
|
|
2
|
+
from typing import TYPE_CHECKING, Any
|
|
3
|
+
|
|
4
|
+
from aiogram.dispatcher.middlewares.base import BaseMiddleware
|
|
5
|
+
from aiogram.types.base import TelegramObject
|
|
6
|
+
|
|
7
|
+
from aiogram_tool.tools.depend.utils.registry_manager import DependRegistryTransaction
|
|
8
|
+
from aiogram_tool.tools.depend.utils.resolver import DependResolver
|
|
9
|
+
from aiogram_tool.tools.depend.utils.stack_manager import AsyncExitStackTransaction
|
|
10
|
+
|
|
11
|
+
from .exit import DependExit
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from aiogram_tool.tools.depend.tool import DependTool
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DependInnerMiddleware(BaseMiddleware):
|
|
18
|
+
"""Class that injects dependencies into the handler"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, depend_tool: "DependTool") -> None:
|
|
21
|
+
self.depend_tool = depend_tool
|
|
22
|
+
|
|
23
|
+
def get_transactions(
|
|
24
|
+
self, data: dict[str, Any]
|
|
25
|
+
) -> tuple[DependRegistryTransaction, AsyncExitStackTransaction]:
|
|
26
|
+
return data.get("request_registry"), data.get("request_stack")
|
|
27
|
+
|
|
28
|
+
def get_handler_callback(self, data: dict[str, Any]) -> Callable:
|
|
29
|
+
return data["handler"].callback
|
|
30
|
+
|
|
31
|
+
async def __call__(
|
|
32
|
+
self,
|
|
33
|
+
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
|
|
34
|
+
event: TelegramObject,
|
|
35
|
+
data: dict[str, Any],
|
|
36
|
+
) -> Any:
|
|
37
|
+
handler_callback = self.get_handler_callback(data)
|
|
38
|
+
req_registry, req_stack = self.get_transactions(data)
|
|
39
|
+
|
|
40
|
+
resolver = DependResolver(
|
|
41
|
+
dependency_override=self.depend_tool.dependency_override,
|
|
42
|
+
scope_registry=self.depend_tool.scope_registry,
|
|
43
|
+
handler_callback=handler_callback,
|
|
44
|
+
registry=req_registry,
|
|
45
|
+
stack=req_stack,
|
|
46
|
+
middleware_data=data.copy(),
|
|
47
|
+
)
|
|
48
|
+
try:
|
|
49
|
+
inject_params = await resolver.resolve_callback_depends()
|
|
50
|
+
except DependExit:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
data.update(inject_params)
|
|
54
|
+
return await handler(event, data)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from collections.abc import Awaitable, Callable
|
|
2
|
+
from typing import TYPE_CHECKING, Any
|
|
3
|
+
|
|
4
|
+
from aiogram.dispatcher.middlewares.base import BaseMiddleware
|
|
5
|
+
from aiogram.types import TelegramObject
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from aiogram_tool.tools.depend.tool import DependTool
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DependOuterMiddleware(BaseMiddleware):
|
|
12
|
+
def __init__(self, depend_tool: "DependTool") -> None:
|
|
13
|
+
self.depend_tool = depend_tool
|
|
14
|
+
|
|
15
|
+
async def __call__(
|
|
16
|
+
self,
|
|
17
|
+
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
|
|
18
|
+
event: TelegramObject,
|
|
19
|
+
data: dict[str, Any],
|
|
20
|
+
) -> Any:
|
|
21
|
+
data["context"] = event
|
|
22
|
+
async with self.depend_tool.registry.transaction() as req_registry:
|
|
23
|
+
async with self.depend_tool.stack_manager.transaction() as req_stack:
|
|
24
|
+
data.update(
|
|
25
|
+
{"request_registry": req_registry, "request_stack": req_stack}
|
|
26
|
+
)
|
|
27
|
+
return await handler(event, data)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from aiogram_tool.tools.depend.types.enums import Scope
|
|
5
|
+
from aiogram_tool.tools.depend.types.exceptions import CallableError
|
|
6
|
+
from aiogram_tool.types import _MISSING
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class From:
|
|
11
|
+
depend: Callable
|
|
12
|
+
scope: Scope | _MISSING = _MISSING
|
|
13
|
+
|
|
14
|
+
def __post_init__(self) -> None:
|
|
15
|
+
if not callable(self.depend):
|
|
16
|
+
raise CallableError(f"object {self.depend} is not callable")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def Depends(depend: Callable, *, scope: Scope | _MISSING = _MISSING) -> From:
|
|
20
|
+
return From(depend=depend, scope=scope)
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from asyncio import Lock
|
|
2
|
+
from collections.abc import Callable, MutableMapping
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from aiogram_tool.types import _MISSING
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DependencyMemoryStorage:
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
self.storage: MutableMapping = {}
|
|
11
|
+
|
|
12
|
+
async def get_value(self, key: Callable) -> Any | _MISSING:
|
|
13
|
+
return self.storage.get(key, _MISSING)
|
|
14
|
+
|
|
15
|
+
async def set_value(self, key: Callable, value: Any) -> None:
|
|
16
|
+
self.storage[key] = value
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DependencyMemoryLockStorage(DependencyMemoryStorage):
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self.global_lock = Lock()
|
|
22
|
+
self.locks: MutableMapping = {}
|
|
23
|
+
super().__init__()
|
|
24
|
+
|
|
25
|
+
async def lock(self, key: Callable) -> Lock:
|
|
26
|
+
async with self.global_lock:
|
|
27
|
+
if key not in self.locks.keys():
|
|
28
|
+
self.locks[key] = Lock()
|
|
29
|
+
return self.locks[key]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
from aiogram import Dispatcher
|
|
4
|
+
from aiogram.dispatcher.event.telegram import TelegramEventObserver
|
|
5
|
+
|
|
6
|
+
from aiogram_tool.tools.depend.types.exceptions import (
|
|
7
|
+
DependencyOverrideError,
|
|
8
|
+
ObserverError,
|
|
9
|
+
)
|
|
10
|
+
from aiogram_tool.tools.depend.utils.registry_manager import (
|
|
11
|
+
DependRegistryTransactionManager,
|
|
12
|
+
)
|
|
13
|
+
from aiogram_tool.tools.depend.utils.scope_registry import ScopeRegistry
|
|
14
|
+
from aiogram_tool.tools.depend.utils.stack_manager import (
|
|
15
|
+
AsyncExitStackTransactionManager,
|
|
16
|
+
)
|
|
17
|
+
from aiogram_tool.tools.setup import BaseTool
|
|
18
|
+
|
|
19
|
+
from .components.inner_middleware import DependInnerMiddleware
|
|
20
|
+
from .components.outer_middleware import DependOuterMiddleware
|
|
21
|
+
from .depend import From
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DependTool(BaseTool):
|
|
25
|
+
"""Class for register dependency middlewares"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
dependency_override: dict[Callable, From] | None = None,
|
|
30
|
+
allowed_updates: list[str] | None = None,
|
|
31
|
+
scope_registry: ScopeRegistry | None = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
if dependency_override is not None:
|
|
34
|
+
for key, depend in dependency_override.items():
|
|
35
|
+
error_msg = None
|
|
36
|
+
if not callable(key):
|
|
37
|
+
error_msg = f"Key {key} in dependency override must be callable"
|
|
38
|
+
|
|
39
|
+
if not isinstance(depend, From):
|
|
40
|
+
error_msg = f"Invalid type in dependency_override {depend}"
|
|
41
|
+
|
|
42
|
+
if error_msg is not None:
|
|
43
|
+
raise DependencyOverrideError(error_msg)
|
|
44
|
+
|
|
45
|
+
self.dependency_override = dependency_override or {}
|
|
46
|
+
self.allowed_updates = allowed_updates
|
|
47
|
+
self.scope_registry = scope_registry or ScopeRegistry()
|
|
48
|
+
self.registry = DependRegistryTransactionManager()
|
|
49
|
+
self.stack_manager = AsyncExitStackTransactionManager()
|
|
50
|
+
|
|
51
|
+
async def shutdown(self) -> None:
|
|
52
|
+
await self.stack_manager.stack.aclose()
|
|
53
|
+
|
|
54
|
+
def setup(self, dispatcher: Dispatcher) -> None:
|
|
55
|
+
dispatcher.shutdown.register(self.shutdown)
|
|
56
|
+
dispatcher.workflow_data["depend_tool"] = self
|
|
57
|
+
|
|
58
|
+
updates = (
|
|
59
|
+
self.allowed_updates
|
|
60
|
+
if self.allowed_updates is not None
|
|
61
|
+
else dispatcher.resolve_used_update_types()
|
|
62
|
+
)
|
|
63
|
+
inner_middleware = DependInnerMiddleware(depend_tool=self)
|
|
64
|
+
outer_middleware = DependOuterMiddleware(depend_tool=self)
|
|
65
|
+
|
|
66
|
+
for update in updates:
|
|
67
|
+
observer: TelegramEventObserver = dispatcher.observers.get(update, None)
|
|
68
|
+
if observer is None:
|
|
69
|
+
raise ObserverError(f"Invalid observer {update}")
|
|
70
|
+
|
|
71
|
+
observer.outer_middleware(outer_middleware)
|
|
72
|
+
observer.middleware(inner_middleware)
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from enum import Enum, auto
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Scope(Enum):
|
|
5
|
+
"""Scopes for dependecies
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
SINGLETON - caching of the dependency result at the full lifecycle level
|
|
9
|
+
REQUEST - caching of a dependency result at the level of a single user request
|
|
10
|
+
TRANSIENT - Dependence is triggered every time.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
SINGLETON = auto()
|
|
14
|
+
REQUEST = auto()
|
|
15
|
+
TRANSIENT = auto()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class DependencyOverrideError(Exception): ...
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ObserverError(Exception): ...
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CallableError(Exception): ...
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DependRecursionError(Exception): ...
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UnsupportedParameterKindError(Exception): ...
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InvalidMiddlewareDataArgumentError(Exception): ...
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ContextManagerError(Exception): ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class InvalidDependencyError(Exception): ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NotFoundDependTool(Exception): ...
|