aiogram_tool 1.0.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Dyadchenko Vladislav Sergeevich
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.3
2
+ Name: aiogram_tool
3
+ Version: 1.0.0
4
+ Summary: Tools which doing work with aiogram simple
5
+ License: LICENSE.md
6
+ Author: Vlad_shayzi1
7
+ Author-email: tecnomega@mail.ru
8
+ Requires-Python: >=3.10
9
+ Classifier: License :: Other/Proprietary License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Dist: aiogram (>=3.21.0,<4.0.0)
16
+ Requires-Dist: redis (>=6.2.0,<7.0.0)
17
+ Description-Content-Type: text/markdown
18
+
19
+ ## aiogram_tool
20
+ It's libriary with tools for aiogram.
21
+
22
+
23
+ # What kind of tools are these?
24
+ - [Dependency Injection](https://github.com/shayzi3/aiogram_tool/blob/master/examples/depend/default.py)
25
+ - [Rate limiter for handlers](https://github.com/shayzi3/aiogram_tool/blob/master/examples/limit/default.py)
26
+
27
+
28
+ ## Installation
29
+
30
+ ### Pip
31
+ ```cmd
32
+ pip install aiogram_tool
33
+ ```
34
+
35
+ ### Poetry
36
+ ```cmd
37
+ poetry add aiogram_tool
38
+ ```
39
+
40
+
41
+ ## Documentation
42
+
43
+ - [RU](https://github.com/shayzi3/aiogram_tool/tree/master/docs/ru)
44
+ - [EN](https://github.com/shayzi3/aiogram_tool/tree/master/docs/en)
45
+
46
+
47
+ ## Features `Depend`
48
+
49
+ - Depend without response for Router, handler
50
+ - Exit from depend before call handler
51
+ - use cache
52
+
@@ -0,0 +1,33 @@
1
+ ## aiogram_tool
2
+ It's libriary with tools for aiogram.
3
+
4
+
5
+ # What kind of tools are these?
6
+ - [Dependency Injection](https://github.com/shayzi3/aiogram_tool/blob/master/examples/depend/default.py)
7
+ - [Rate limiter for handlers](https://github.com/shayzi3/aiogram_tool/blob/master/examples/limit/default.py)
8
+
9
+
10
+ ## Installation
11
+
12
+ ### Pip
13
+ ```cmd
14
+ pip install aiogram_tool
15
+ ```
16
+
17
+ ### Poetry
18
+ ```cmd
19
+ poetry add aiogram_tool
20
+ ```
21
+
22
+
23
+ ## Documentation
24
+
25
+ - [RU](https://github.com/shayzi3/aiogram_tool/tree/master/docs/ru)
26
+ - [EN](https://github.com/shayzi3/aiogram_tool/tree/master/docs/en)
27
+
28
+
29
+ ## Features `Depend`
30
+
31
+ - Depend without response for Router, handler
32
+ - Exit from depend before call handler
33
+ - use cache
File without changes
@@ -0,0 +1,10 @@
1
+ from .depend import Depend
2
+ from .components.filter import DependFilter
3
+ from .setup import setup_depend_tool
4
+
5
+
6
+ __all__ = [
7
+ "Depend",
8
+ "DependFilter",
9
+ "setup_depend_tool",
10
+ ]
@@ -0,0 +1,28 @@
1
+ from typing import Any
2
+
3
+ from aiogram.filters import Filter
4
+ from aiogram import Dispatcher
5
+ from aiogram.dispatcher.event.handler import HandlerObject
6
+
7
+ from ..depend import Depend
8
+ from ..utils.inject import inject_parametrs
9
+
10
+
11
+
12
+
13
+ class DependFilter(Filter):
14
+
15
+
16
+ async def __call__(self, *args: Any, **kwargs: dict[str, Any]) -> dict[str, Any]:
17
+ handler_object: HandlerObject = kwargs.get("handler")
18
+ dispatcher: Dispatcher = kwargs.get("dispatcher")
19
+
20
+ dependency_override: dict[str, Depend] = dispatcher.workflow_data.get("dependency_override", {})
21
+
22
+ kwargs.update({"event": args[0]})
23
+ depend_kwargs = await inject_parametrs(
24
+ callback=handler_object.callback,
25
+ dependency_override=dependency_override,
26
+ middleware_data=kwargs
27
+ )
28
+ return depend_kwargs
@@ -0,0 +1,36 @@
1
+ from typing import Any, Callable, Awaitable
2
+
3
+ from aiogram.dispatcher.middlewares.base import BaseMiddleware
4
+ from aiogram.types.base import TelegramObject
5
+ from aiogram import Dispatcher
6
+ from aiogram.dispatcher.event.handler import HandlerObject
7
+
8
+ from ..depend import Depend
9
+ from ..utils.inject import inject_parametrs
10
+
11
+
12
+
13
+
14
+
15
+ class DependMiddleware(BaseMiddleware):
16
+
17
+
18
+ async def __call__(
19
+ self,
20
+ handler: Callable[[TelegramObject, dict[str, Any]], Awaitable],
21
+ event: TelegramObject,
22
+ data: dict[str, Any]
23
+ ):
24
+ handler_object: HandlerObject = data.get("handler")
25
+ dispatcher: Dispatcher = data.get("dispatcher")
26
+
27
+ dependency_override: dict[str, Depend] = dispatcher.workflow_data.get("dependency_override", {})
28
+
29
+ data.update({"event": event})
30
+ inject = await inject_parametrs(
31
+ callback=handler_object.callback,
32
+ dependency_override=dependency_override,
33
+ middleware_data=data
34
+ )
35
+ data.update(inject)
36
+ return await handler(event, data)
@@ -0,0 +1,103 @@
1
+ import inspect
2
+
3
+ from contextlib import (
4
+ asynccontextmanager,
5
+ contextmanager
6
+ )
7
+ from typing import (
8
+ Callable,
9
+ Any,
10
+ _AnnotatedAlias
11
+ )
12
+
13
+
14
+
15
+ class Depend:
16
+ def __init__(self, obj: Callable) -> None:
17
+ self.obj = obj
18
+ self.isgenerator = False
19
+ self.isasync = False
20
+ self.obj_name = getattr(obj, "__name__", "")
21
+ self.arguments = {}
22
+
23
+ if isinstance(self.obj, type):
24
+ raise TypeError("dont valid type. Initialize class")
25
+
26
+ if not callable(self.obj):
27
+ raise TypeError("obj must be a callable")
28
+
29
+ if not inspect.isfunction(self.obj):
30
+ self.obj_name = self.obj.__class__.__name__
31
+ self.obj = getattr(self.obj, "__call__")
32
+
33
+ self.__explore_obj()
34
+ self.__explore_signature()
35
+
36
+
37
+ def __explore_obj(self) -> None:
38
+ if inspect.iscoroutinefunction(self.obj):
39
+ self.isasync = True
40
+
41
+ else:
42
+ if inspect.isasyncgenfunction(self.obj):
43
+ self.isgenerator = True
44
+ self.isasync = True
45
+ self.obj = asynccontextmanager(self.obj)
46
+
47
+ elif inspect.isgeneratorfunction(self.obj):
48
+ self.isgenerator = True
49
+ self.obj = contextmanager(self.obj)
50
+
51
+ else:
52
+ types = []
53
+ for closure in inspect.getclosurevars(self.obj):
54
+ if isinstance(closure, dict):
55
+ types.extend(list(closure.values()))
56
+
57
+ for type_ in types:
58
+ name = getattr(type_, "__name__", None)
59
+ if name == "_AsyncGeneratorContextManager":
60
+ self.isasync = True
61
+ self.isgenerator = True
62
+
63
+ elif name == "_GeneratorContextManager":
64
+ self.isgenerator = True
65
+
66
+
67
+ def __explore_signature(self) -> None:
68
+ signature = inspect.signature(self.obj)
69
+ for key, annotation in signature.parameters.items():
70
+ default_value = annotation.default
71
+
72
+ if isinstance(annotation.annotation, _AnnotatedAlias):
73
+ types = getattr(annotation.annotation, "__metadata__")
74
+ for type_ in types:
75
+ if isinstance(type_, Depend):
76
+ self.arguments[key] = type_
77
+
78
+ elif isinstance(default_value, Depend):
79
+ self.arguments[key] = default_value
80
+
81
+ else:
82
+ self.arguments[key] = annotation.annotation
83
+
84
+
85
+ async def call(self, middleware_data: dict[str, Any]) -> Any:
86
+ kwargs = {}
87
+ for key, value in self.arguments.items():
88
+ if isinstance(value, Depend):
89
+ kwargs[key] = await value.call(middleware_data)
90
+ else:
91
+ kwargs[key] = middleware_data.get(key)
92
+
93
+ if self.isasync is False:
94
+ if self.isgenerator is True:
95
+ with self.obj(**kwargs) as value:
96
+ return value
97
+ return self.obj(**kwargs)
98
+
99
+ else:
100
+ if self.isgenerator is True:
101
+ async with self.obj(**kwargs) as asvalue:
102
+ return asvalue
103
+ return await self.obj(**kwargs)
@@ -0,0 +1,34 @@
1
+ from aiogram import Dispatcher
2
+ from aiogram.dispatcher.event.telegram import TelegramEventObserver
3
+
4
+ from .depend import Depend
5
+ from .components.middleware import DependMiddleware
6
+
7
+
8
+
9
+
10
+
11
+ def setup_depend_tool(
12
+ dispatcher: Dispatcher,
13
+ dependency_override: dict[str, Depend] = {},
14
+ allowed_updates: list[str] = [],
15
+ middleware: bool = True,
16
+ ) -> None:
17
+ if not isinstance(dispatcher, Dispatcher):
18
+ raise TypeError("dispatcher must be Dispatcher type")
19
+
20
+ if dependency_override:
21
+ for dep in dependency_override.values():
22
+ if not isinstance(dep, Depend):
23
+ raise TypeError(f"Invalid type in dependency_override {dep}")
24
+
25
+ if allowed_updates:
26
+ for allow in allowed_updates:
27
+ if allow not in dispatcher.observers:
28
+ raise TypeError(f"observer {allow} not exists")
29
+
30
+ if middleware is True:
31
+ for allow in dispatcher.resolve_used_update_types() or allowed_updates:
32
+ observer: TelegramEventObserver = getattr(dispatcher, allow)
33
+ observer.middleware(DependMiddleware())
34
+ dispatcher.__setitem__("dependency_override", dependency_override)
@@ -0,0 +1,33 @@
1
+ import inspect
2
+
3
+ from typing import Any, Callable, _AnnotatedAlias
4
+
5
+ from ..depend import Depend
6
+
7
+
8
+
9
+ async def inject_parametrs(
10
+ callback: Callable,
11
+ middleware_data: dict[str, Any],
12
+ dependency_override: dict[str, Depend] = {},
13
+ ) -> dict[str, Any]:
14
+ data = {}
15
+ signature = inspect.signature(callback)
16
+ for key, annotation in signature.parameters.items():
17
+ default_value = annotation.default
18
+
19
+ if isinstance(annotation.annotation, _AnnotatedAlias):
20
+ types = getattr(annotation.annotation, "__metadata__")
21
+ for type_ in types:
22
+ if isinstance(type_, Depend):
23
+ if type_.obj_name in dependency_override.keys():
24
+ data[key] = await dependency_override[type_.obj_name].call(middleware_data)
25
+ else:
26
+ data[key] = await type_.call(middleware_data)
27
+
28
+ elif isinstance(default_value, Depend):
29
+ if default_value.obj_name in dependency_override.keys():
30
+ data[key] = await dependency_override[default_value.obj_name].call(middleware_data)
31
+ else:
32
+ data[key] = await default_value.call(middleware_data)
33
+ return data
@@ -0,0 +1,7 @@
1
+ from .limit import Limit
2
+ from .setup import setup_limit_tool
3
+
4
+ __all__ = [
5
+ "Limit",
6
+ "setup_limit_tool"
7
+ ]
@@ -0,0 +1,87 @@
1
+ import inspect
2
+
3
+ from typing import Callable, Awaitable
4
+ from datetime import datetime, timedelta
5
+
6
+ from aiogram import Dispatcher
7
+ from aiogram.filters import Filter
8
+ from aiogram.types.base import TelegramObject
9
+ from aiogram.dispatcher.event.handler import HandlerObject
10
+
11
+ from .storage.abstract_storage import AbstractStorage
12
+ from .storage import MemoryStorage
13
+
14
+
15
+
16
+
17
+ class Limit(Filter):
18
+
19
+ def __init__(
20
+ self,
21
+ seconds: float = 0,
22
+ minutes: float = 0,
23
+ hours: float = 0,
24
+ days: float = 0,
25
+ all_users: bool = False,
26
+ storage: AbstractStorage | None = None,
27
+ answer_callback: Callable[[TelegramObject, timedelta, datetime], Awaitable] | None = None
28
+ ) -> None:
29
+ if not isinstance(all_users, bool):
30
+ raise TypeError("all_users must be bool type")
31
+
32
+ if storage is not None:
33
+ if not issubclass(type(storage), AbstractStorage):
34
+ raise TypeError(f"Invalid type for storage")
35
+
36
+ if answer_callback is not None:
37
+ if not inspect.iscoroutinefunction(answer_callback):
38
+ raise TypeError(f"answer_callback must be croutine function")
39
+
40
+ # checks types
41
+ self.storage = storage
42
+ self.answer_callback = answer_callback
43
+ self.all_users = all_users
44
+
45
+ self.time = timedelta(
46
+ seconds=seconds,
47
+ minutes=minutes,
48
+ hours=hours,
49
+ days=days
50
+ )
51
+
52
+
53
+ async def __call__(
54
+ self,
55
+ message: TelegramObject,
56
+ dispatcher: Dispatcher,
57
+ handler: HandlerObject
58
+ ) -> bool:
59
+ storage = dispatcher.workflow_data.get("storage", MemoryStorage())
60
+ answer_callback = dispatcher.workflow_data.get("answer_callback")
61
+
62
+ if self.storage is not None:
63
+ storage = self.storage
64
+
65
+ if self.answer_callback is not None:
66
+ answer_callback = self.answer_callback
67
+
68
+ query = str(message.from_user.id) + "@" + handler.callback.__name__
69
+ if self.all_users is True:
70
+ query = "users" + "@" + handler.callback.__name__
71
+
72
+ user_time = await storage.get(query)
73
+ if user_time is None:
74
+ await storage.update(query, datetime.utcnow() + self.time)
75
+ return True
76
+
77
+ if user_time >= datetime.utcnow():
78
+ time_last = user_time - datetime.utcnow()
79
+ if answer_callback is not None:
80
+ await answer_callback(message, self.time, time_last)
81
+ else:
82
+ await message.answer(f"Retry after {round(time_last.total_seconds(), 0)} seconds")
83
+ return False
84
+
85
+ await storage.update(query, datetime.utcnow() + self.time)
86
+ return True
87
+
@@ -0,0 +1,27 @@
1
+ import inspect
2
+
3
+ from typing import Callable, Awaitable
4
+ from aiogram.types import Message
5
+ from aiogram import Dispatcher
6
+
7
+ from datetime import datetime, timedelta
8
+
9
+ from .storage.abstract_storage import AbstractStorage
10
+ from .storage import MemoryStorage
11
+
12
+
13
+
14
+ def setup_limit_tool(
15
+ dispatcher: Dispatcher,
16
+ storage: AbstractStorage = MemoryStorage(),
17
+ answer_callback: Callable[[Message, timedelta, datetime], Awaitable] | None = None
18
+ ) -> None:
19
+ if not issubclass(type(storage), AbstractStorage):
20
+ raise TypeError(f"Invalid type for storage")
21
+
22
+ if answer_callback is not None:
23
+ if not inspect.iscoroutinefunction(answer_callback):
24
+ raise TypeError(f"answer_callback must be croutine function")
25
+
26
+ dispatcher.__setitem__("storage", storage)
27
+ dispatcher.__setitem__("answer_callback", answer_callback)
@@ -0,0 +1,7 @@
1
+ from .memory_storage import MemoryStorage
2
+ from .redis_storage import RedisStorage
3
+
4
+
5
+ __all__ = [
6
+ "RedisStorage",
7
+ ]
@@ -0,0 +1,16 @@
1
+ from abc import ABC, abstractmethod
2
+ from datetime import datetime
3
+
4
+
5
+
6
+ class AbstractStorage(ABC):
7
+
8
+
9
+ @abstractmethod
10
+ async def get(self, name: str) -> datetime | None:
11
+ raise NotImplementedError
12
+
13
+
14
+ @abstractmethod
15
+ async def update(self, name: str, value: datetime) -> None:
16
+ raise NotImplementedError
@@ -0,0 +1,23 @@
1
+ from datetime import datetime
2
+
3
+ from .abstract_storage import AbstractStorage
4
+
5
+
6
+
7
+ class DataStorage:
8
+ data: dict[str, datetime] = {}
9
+
10
+
11
+
12
+
13
+ class MemoryStorage(AbstractStorage):
14
+ def __init__(self):
15
+ self.storage = DataStorage
16
+
17
+
18
+ async def get(self, name: str) -> datetime | None:
19
+ return self.storage.data.get(name)
20
+
21
+
22
+ async def update(self, name: str, value: datetime) -> None:
23
+ self.storage.data.update({name: value})
@@ -0,0 +1,37 @@
1
+ import json
2
+
3
+ from datetime import datetime
4
+ from redis.asyncio import Redis as AsyncRedis
5
+
6
+
7
+ from .abstract_storage import AbstractStorage
8
+
9
+
10
+
11
+
12
+ class RedisStorage(AbstractStorage):
13
+ def __init__(self, redis: AsyncRedis):
14
+ if not isinstance(redis, AsyncRedis):
15
+ raise TypeError("Invalid type for argument redis")
16
+
17
+ self.redis = redis
18
+
19
+
20
+ async def get(self, name: str) -> datetime | None:
21
+ async with self.redis as session:
22
+ data = await session.hget(name="limit_tool", key=name)
23
+ if data is None:
24
+ return None
25
+
26
+ if isinstance(data, bytes):
27
+ data = data.decode()
28
+ return datetime.fromisoformat(data)
29
+
30
+
31
+ async def update(self, name: str, value: datetime) -> None:
32
+ async with self.redis as session:
33
+ await session.hset(
34
+ name="limit_tool",
35
+ key=name,
36
+ value=value.isoformat()
37
+ )
@@ -0,0 +1,19 @@
1
+ [project]
2
+ name = "aiogram_tool"
3
+ version = "1.0.0"
4
+ description = "Tools which doing work with aiogram simple"
5
+ authors = [
6
+ {name = "Vlad_shayzi1",email = "tecnomega@mail.ru"}
7
+ ]
8
+ license = "LICENSE.md"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "aiogram (>=3.21.0,<4.0.0)",
13
+ "redis (>=6.2.0,<7.0.0)"
14
+ ]
15
+
16
+
17
+ [build-system]
18
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
19
+ build-backend = "poetry.core.masonry.api"