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.
Files changed (47) hide show
  1. aiogram_tool/__init__.py +0 -0
  2. aiogram_tool/storage/__init__.py +15 -0
  3. aiogram_tool/storage/base.py +20 -0
  4. aiogram_tool/storage/impl/__init__.py +0 -0
  5. aiogram_tool/storage/impl/file.py +72 -0
  6. aiogram_tool/storage/impl/memory.py +33 -0
  7. aiogram_tool/storage/impl/redis.py +39 -0
  8. aiogram_tool/tools/__init__.py +0 -0
  9. aiogram_tool/tools/callback_data/__init__.py +4 -0
  10. aiogram_tool/tools/callback_data/answer.py +10 -0
  11. aiogram_tool/tools/callback_data/filter.py +120 -0
  12. aiogram_tool/tools/depend/__init__.py +16 -0
  13. aiogram_tool/tools/depend/components/__init__.py +0 -0
  14. aiogram_tool/tools/depend/components/exit.py +4 -0
  15. aiogram_tool/tools/depend/components/filter.py +63 -0
  16. aiogram_tool/tools/depend/components/inner_middleware.py +54 -0
  17. aiogram_tool/tools/depend/components/outer_middleware.py +27 -0
  18. aiogram_tool/tools/depend/depend.py +20 -0
  19. aiogram_tool/tools/depend/storage/__init__.py +0 -0
  20. aiogram_tool/tools/depend/storage/memory.py +29 -0
  21. aiogram_tool/tools/depend/tool.py +72 -0
  22. aiogram_tool/tools/depend/types/__init__.py +0 -0
  23. aiogram_tool/tools/depend/types/enums.py +15 -0
  24. aiogram_tool/tools/depend/types/exceptions.py +25 -0
  25. aiogram_tool/tools/depend/types/schema.py +19 -0
  26. aiogram_tool/tools/depend/utils/__init__.py +0 -0
  27. aiogram_tool/tools/depend/utils/inspect.py +56 -0
  28. aiogram_tool/tools/depend/utils/registry_manager.py +64 -0
  29. aiogram_tool/tools/depend/utils/resolver.py +163 -0
  30. aiogram_tool/tools/depend/utils/scope_registry.py +42 -0
  31. aiogram_tool/tools/depend/utils/stack_manager.py +35 -0
  32. aiogram_tool/tools/limit/__init__.py +5 -0
  33. aiogram_tool/tools/limit/answer.py +20 -0
  34. aiogram_tool/tools/limit/filter.py +82 -0
  35. aiogram_tool/tools/limit/rate_limit/__init__.py +9 -0
  36. aiogram_tool/tools/limit/rate_limit/base.py +36 -0
  37. aiogram_tool/tools/limit/rate_limit/fixed_window.py +70 -0
  38. aiogram_tool/tools/limit/rate_limit/sliding_window.py +79 -0
  39. aiogram_tool/tools/limit/rate_limit/token_bucket.py +102 -0
  40. aiogram_tool/tools/limit/schema.py +24 -0
  41. aiogram_tool/tools/limit/tool.py +21 -0
  42. aiogram_tool/tools/setup.py +25 -0
  43. aiogram_tool/types.py +1 -0
  44. aiogram_tool-1.2.2.dist-info/METADATA +235 -0
  45. aiogram_tool-1.2.2.dist-info/RECORD +47 -0
  46. aiogram_tool-1.2.2.dist-info/WHEEL +4 -0
  47. aiogram_tool-1.2.2.dist-info/licenses/LICENSE.md +21 -0
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,4 @@
1
+ from .answer import CallbackDataAnswer
2
+ from .filter import LongCallbackData
3
+
4
+ __all__ = ["LongCallbackData", "CallbackDataAnswer"]
@@ -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,4 @@
1
+ class DependExit(Exception):
2
+ """If an error occurs in a dependency, the handler will not be called."""
3
+
4
+ ...
@@ -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): ...