aiogram-testing 1.1.0__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.
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.4
2
+ Name: aiogram-testing
3
+ Version: 1.1.0
4
+ Summary: A library for testing your bots on aiogram
5
+ License: MIT
6
+ Author: k0te1ch
7
+ Author-email: khvostov40@gmail.com
8
+ Requires-Python: >=3.12,<3.15
9
+ Classifier: Framework :: AsyncIO
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Software Development :: Testing
17
+ Requires-Dist: aiogram (>=3.28,<4.0)
18
+ Project-URL: Homepage, https://github.com/k0te1ch/aiogram_tests
19
+ Project-URL: Repository, https://github.com/k0te1ch/aiogram_tests
20
+ Description-Content-Type: text/markdown
21
+
22
+ # Aiogram Tests
23
+
24
+ ***aiogram_tests*** is a testing library for bots written on [aiogram]("https://github.com/aiogram/aiogram")</a>
25
+
26
+ ## 📦 Installation
27
+
28
+ ```bash
29
+ pip install aiogram-testing
30
+ ```
31
+
32
+ The import name stays `aiogram_tests`:
33
+
34
+ ```python
35
+ from aiogram_tests import MockedRequester
36
+ ```
37
+
38
+ ## 📚 Simple examples
39
+
40
+ ### Simple handler test
41
+
42
+ #### Simple bot
43
+
44
+ ```python
45
+ from aiogram import Bot, Dispatcher, types
46
+ from aiogram.fsm.context import FSMContext
47
+
48
+ # Please, keep your bot tokens on environments, this code only example
49
+ bot = Bot('123456789:AABBCCDDEEFFaabbccddeeff-1234567890')
50
+ dp = Dispatcher()
51
+
52
+
53
+ @dp.message()
54
+ async def echo(message: types.Message, state: FSMContext) -> None:
55
+ await message.answer(message.text)
56
+
57
+
58
+ if __name__ == '__main__':
59
+ dp.run_polling(bot)
60
+
61
+
62
+ ```
63
+
64
+ #### Test cases
65
+
66
+ ```python
67
+ import pytest
68
+
69
+ from bot import echo
70
+
71
+ from aiogram_tests import MockedRequester
72
+ from aiogram_tests.handler import MessageHandler
73
+ from aiogram_tests.types.dataset import MESSAGE
74
+
75
+
76
+ @pytest.mark.asyncio
77
+ async def test_echo():
78
+ request = MockedRequester(MessageHandler(echo))
79
+ calls = await request.query(message=MESSAGE.as_object(text="Hello, Bot!"))
80
+ answer_message = calls.send_message.fetchone()
81
+ assert answer_message.text == "Hello, Bot!"
82
+
83
+ ```
84
+
85
+ ### [▶️ More]("https://github.com/aiogram-tests/aiogram_tests/tree/master/examples") examples
86
+
@@ -0,0 +1,14 @@
1
+ aiogram_tests/__init__.py,sha256=JOazEvtgjekpejpdLS43BgW-0u_vNzLxV_e2V8CFEGY,92
2
+ aiogram_tests/exceptions.py,sha256=cgyhFFntzudw_3Lw6QwpC9nKtMr_mMSQPUGbBDKRybQ,50
3
+ aiogram_tests/handler/__init__.py,sha256=ftP0YTO-L8NMVNKv2mXlHas-TDlwv7jB6chmZ3QXZck,265
4
+ aiogram_tests/handler/base.py,sha256=0JXRTsmmh26AUQMZd_qWtCOQiMYaIuHbJD_tUY8fPG4,2667
5
+ aiogram_tests/handler/handler.py,sha256=pHTjSOYsOa34AKcok9ocyt1Dpe3X8EAR8MILcWWZ0nA,2773
6
+ aiogram_tests/mocked_bot.py,sha256=2xdajeMDz3arc8C1eh6ghDgvUBpC-IMh9hfId3Fcni0,3413
7
+ aiogram_tests/requester.py,sha256=IUM_OyNe82219bQJpyJxMrWC3qW2tbAViaCMMBY2Co4,2745
8
+ aiogram_tests/types/__init__.py,sha256=1mKDyeY_uRovMCC49M3Ow3WXGAxkrbyqsw9G-WEWrDY,46
9
+ aiogram_tests/types/dataset/__init__.py,sha256=RSPtOgPhsUU1T9buRb6VO8rd_Ayt7PU8TD7_5NcySIw,14718
10
+ aiogram_tests/types/dataset/base.py,sha256=7oJKipuldXkpsOG20bDfttBHtFqFiWPCSArA9Tkqdq0,1987
11
+ aiogram_tests/utils.py,sha256=PnKkxzEQ6vXcXXPnpTbyjlv4RoJzoTTLyj4MathU09M,112
12
+ aiogram_testing-1.1.0.dist-info/METADATA,sha256=Y8gaA0QGL4RyHqfDczcbLyIPyykAECC708QsbIbqVuA,2167
13
+ aiogram_testing-1.1.0.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
14
+ aiogram_testing-1.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.4.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ from .requester import MockedRequester
2
+
3
+ __all__ = ["MockedRequester"]
4
+ __version__ = "1.1.0"
@@ -0,0 +1,2 @@
1
+ class MethodIsNotCalledError(Exception):
2
+ pass
@@ -0,0 +1,6 @@
1
+ from .base import RequestHandler
2
+ from .handler import CallbackQueryHandler
3
+ from .handler import MessageHandler
4
+ from .handler import TelegramEventObserverHandler
5
+
6
+ __all__ = ["MessageHandler", "CallbackQueryHandler", "TelegramEventObserverHandler", "RequestHandler"]
@@ -0,0 +1,79 @@
1
+ from collections.abc import Iterable
2
+
3
+ from aiogram import BaseMiddleware
4
+ from aiogram import Dispatcher
5
+ from aiogram.dispatcher.event.telegram import TelegramEventObserver
6
+ from aiogram.fsm.storage.memory import MemoryStorage
7
+ from aiogram.methods import TelegramMethod
8
+ from aiogram.methods.base import Response
9
+ from aiogram.methods.base import TelegramType
10
+
11
+ from aiogram_tests.mocked_bot import DEFAULT_AUTO_MOCK_SUCCESS
12
+ from aiogram_tests.mocked_bot import MockedBot
13
+
14
+
15
+ class RequestHandler:
16
+ def __init__(
17
+ self,
18
+ dp_middlewares: Iterable[BaseMiddleware] = None,
19
+ exclude_observer_methods: Iterable[str] = None,
20
+ auto_mock_success: bool = DEFAULT_AUTO_MOCK_SUCCESS,
21
+ dp: Dispatcher | None = None,
22
+ **kwargs,
23
+ ):
24
+ self.bot = MockedBot(auto_mock_success=auto_mock_success)
25
+ if dp is None:
26
+ dp = Dispatcher(storage=MemoryStorage())
27
+ self.dp = dp
28
+
29
+ if dp_middlewares is None:
30
+ dp_middlewares = ()
31
+
32
+ if exclude_observer_methods is None:
33
+ exclude_observer_methods = []
34
+
35
+ dispatcher_methods = self._get_dispatcher_event_observers()
36
+ available_methods = tuple(set(dispatcher_methods) - set(exclude_observer_methods))
37
+ self._register_middlewares(available_methods, tuple(dp_middlewares))
38
+
39
+ def _get_dispatcher_event_observers(self) -> list[str]:
40
+ """
41
+ Returns a names for bot event observers, like message, callback_query etc.
42
+ """
43
+
44
+ result = []
45
+ for name in dir(self.dp):
46
+ if isinstance(getattr(self.dp, name), TelegramEventObserver):
47
+ result.append(name)
48
+
49
+ return result
50
+
51
+ def _register_middlewares(self, event_observer: Iterable, middlewares: Iterable) -> None:
52
+ for eo_name in event_observer:
53
+ for m in middlewares:
54
+ eo_obj = getattr(self.dp, eo_name)
55
+ eo_obj.middleware.register(m)
56
+
57
+ async def __call__(self, *args, **kwargs):
58
+ raise NotImplementedError
59
+
60
+ def add_result_for(
61
+ self,
62
+ method: type[TelegramMethod[TelegramType]],
63
+ ok: bool,
64
+ result: TelegramType = None,
65
+ description: str | None = None,
66
+ error_code: int = 200,
67
+ migrate_to_chat_id: int | None = None,
68
+ retry_after: int | None = None,
69
+ ) -> Response[TelegramType]:
70
+ response = self.bot.add_result_for(
71
+ method=method,
72
+ ok=ok,
73
+ result=result,
74
+ description=description,
75
+ error_code=error_code,
76
+ migrate_to_chat_id=migrate_to_chat_id,
77
+ retry_after=retry_after,
78
+ )
79
+ return response
@@ -0,0 +1,86 @@
1
+ from collections.abc import Callable
2
+ from collections.abc import Iterable
3
+
4
+ from aiogram import types
5
+ from aiogram.filters import Filter
6
+ from aiogram.filters import StateFilter
7
+ from aiogram.fsm.context import FSMContext
8
+ from aiogram.fsm.state import State
9
+
10
+ from .base import RequestHandler
11
+
12
+
13
+ class TelegramEventObserverHandler(RequestHandler):
14
+ def __init__(
15
+ self,
16
+ callback: Callable,
17
+ *filters: Filter,
18
+ state: State | str | None = None,
19
+ state_data: dict = None,
20
+ state_context: FSMContext | None = None,
21
+ dp_middlewares: Iterable = None,
22
+ exclude_observer_methods: Iterable = None,
23
+ **kwargs,
24
+ ):
25
+ super().__init__(dp_middlewares, exclude_observer_methods, **kwargs)
26
+
27
+ self._callback = callback
28
+ self._filters: list = list(filters)
29
+ self._state: State | str | None = state
30
+ self._state_data: dict = state_data
31
+ self._state_context: FSMContext = state_context
32
+
33
+ if self._state_context:
34
+ self._state_context = state_context
35
+
36
+ if self._state_data is None:
37
+ self._state_data = {}
38
+
39
+ if self._filters is None:
40
+ self._filters = []
41
+
42
+ if not isinstance(self._state_data, dict):
43
+ raise ValueError("state_data is not a dict")
44
+
45
+ async def __call__(self, *args, **kwargs):
46
+ if self._state:
47
+ self._filters.append(StateFilter(self._state))
48
+
49
+ self.register_handler()
50
+
51
+ if self._state_context or self._state:
52
+ state = self.dp.fsm.get_context(self.bot, user_id=12345678, chat_id=12345678)
53
+ await state.set_state(self._state)
54
+ await state.update_data(**self._state_data)
55
+
56
+ await self.feed_update(*args, **kwargs)
57
+
58
+ def register_handler(self) -> None:
59
+ """
60
+ Register TelegramEventObserver in dispatcher
61
+ """
62
+
63
+ raise NotImplementedError
64
+
65
+ async def feed_update(self, *args, **kwargs) -> None:
66
+ """
67
+ Feed dispatcher updates
68
+ """
69
+
70
+ raise NotImplementedError
71
+
72
+
73
+ class MessageHandler(TelegramEventObserverHandler):
74
+ def register_handler(self) -> None:
75
+ self.dp.message.register(self._callback, *self._filters)
76
+
77
+ async def feed_update(self, message: types.Message, *args, **kwargs) -> None:
78
+ await self.dp.feed_update(self.bot, types.Update(update_id=12345678, message=message))
79
+
80
+
81
+ class CallbackQueryHandler(TelegramEventObserverHandler):
82
+ def register_handler(self) -> None:
83
+ self.dp.callback_query.register(self._callback, *self._filters)
84
+
85
+ async def feed_update(self, callback_query: types.CallbackQuery, *args, **kwargs) -> None:
86
+ await self.dp.feed_update(self.bot, types.Update(update_id=12345678, callback_query=callback_query))
@@ -0,0 +1,104 @@
1
+ from collections import deque
2
+ from collections.abc import AsyncGenerator
3
+
4
+ from aiogram import Bot
5
+ from aiogram.client.session.base import BaseSession
6
+ from aiogram.methods import TelegramMethod
7
+ from aiogram.methods.base import Request
8
+ from aiogram.methods.base import Response
9
+ from aiogram.methods.base import TelegramType
10
+ from aiogram.types import ResponseParameters
11
+ from aiogram.types import User
12
+ from aiogram.types.base import UNSET_TYPE
13
+
14
+ DEFAULT_AUTO_MOCK_SUCCESS = True
15
+
16
+
17
+ class MockedSession(BaseSession):
18
+ def __init__(self):
19
+ super().__init__()
20
+ self.responses: deque[Response[TelegramType]] = deque()
21
+ self.requests: deque[Request] = deque()
22
+ self.closed = True
23
+
24
+ def add_result(self, response: Response[TelegramType]) -> Response[TelegramType]:
25
+ self.responses.appendleft(response)
26
+ return response
27
+
28
+ def get_request(self) -> Request | None:
29
+ if self.requests:
30
+ return self.requests[-1]
31
+
32
+ return None
33
+
34
+ async def close(self):
35
+ self.closed = True
36
+
37
+ async def make_request(
38
+ self,
39
+ bot: Bot,
40
+ method: TelegramMethod[TelegramType],
41
+ timeout: int | None = UNSET_TYPE,
42
+ ) -> TelegramType:
43
+ self.closed = False
44
+ request = Request(method=method.__api_method__, data=method.__dict__, files=None)
45
+ self.requests.append(request)
46
+ response: Response[TelegramType] = self.responses.pop()
47
+ self.check_response(
48
+ method=method,
49
+ status_code=response.error_code,
50
+ content=response.model_dump_json(),
51
+ bot=bot,
52
+ )
53
+ return response.result # type: ignore
54
+
55
+ async def stream_content(
56
+ self, url: str, timeout: int, chunk_size: int
57
+ ) -> AsyncGenerator[bytes, None]: # pragma: no cover
58
+ yield b""
59
+
60
+
61
+ class MockedBot(Bot):
62
+ def __init__(self, auto_mock_success: bool = DEFAULT_AUTO_MOCK_SUCCESS, **kwargs):
63
+ super().__init__(kwargs.pop("token", "42:TEST"), session=MockedSession(), **kwargs)
64
+ self.session = MockedSession()
65
+ self._me = User(
66
+ id=self.id,
67
+ is_bot=True,
68
+ first_name="FirstName",
69
+ last_name="LastName",
70
+ username="username",
71
+ language_code="ru",
72
+ )
73
+ self.auto_mock_success = auto_mock_success
74
+
75
+ def add_result_for(
76
+ self,
77
+ method: type[TelegramMethod[TelegramType]],
78
+ ok: bool,
79
+ result: TelegramType = None,
80
+ description: str | None = None,
81
+ error_code: int = 200,
82
+ migrate_to_chat_id: int | None = None,
83
+ retry_after: int | None = None,
84
+ ) -> Response[TelegramType]:
85
+ response = Response[method.__returning__]( # type: ignore
86
+ ok=ok,
87
+ result=result,
88
+ description=description,
89
+ error_code=error_code,
90
+ parameters=ResponseParameters(
91
+ migrate_to_chat_id=migrate_to_chat_id,
92
+ retry_after=retry_after,
93
+ ),
94
+ )
95
+ self.session.add_result(response)
96
+ return response
97
+
98
+ async def __call__(self, method: TelegramMethod, request_timeout: int | None = None):
99
+ if self.auto_mock_success:
100
+ self.add_result_for(method.__class__, ok=True)
101
+ return await super().__call__(method, request_timeout)
102
+
103
+ def get_request(self) -> Request:
104
+ return self.session.get_request()
@@ -0,0 +1,94 @@
1
+ from aiogram.methods import TelegramMethod
2
+ from aiogram.methods.base import Response
3
+ from aiogram.methods.base import TelegramType
4
+
5
+ from .exceptions import MethodIsNotCalledError
6
+ from .handler.base import RequestHandler
7
+ from .utils import camel_case2snake_case
8
+
9
+
10
+ class CallsList(list):
11
+ def fetchone(self):
12
+ if len(self) > 0:
13
+ return self[-1]
14
+ else:
15
+ return None
16
+
17
+ def fetchall(self):
18
+ return self
19
+
20
+
21
+ class Calls:
22
+ def _get_attributes(self):
23
+ res = []
24
+ for item in dir(self):
25
+ if item.startswith("_") or item.endswith("_"):
26
+ continue
27
+
28
+ if not callable(getattr(self, item)):
29
+ res.append(item)
30
+
31
+ return tuple(res)
32
+
33
+ def __getattr__(self, item):
34
+ if item in dir(self):
35
+ return getattr(self, item)
36
+ else:
37
+ raise MethodIsNotCalledError(
38
+ f"method '{item}' is not called by bot, so you cant to get this attribute. "
39
+ f"Called methods: {self._get_attributes()}"
40
+ )
41
+
42
+
43
+ class MockedRequester:
44
+ def __init__(self, request_handler: RequestHandler):
45
+ self._handler: RequestHandler = request_handler
46
+
47
+ async def query(self, *args, **kwargs) -> Calls:
48
+ try:
49
+ await self._handler(*args, **kwargs)
50
+ except TypeError as e:
51
+ raise AttributeError(f"incorrect argument name. {e}")
52
+
53
+ requests = self._handler.bot.session.requests
54
+ result = {}
55
+ for r in requests:
56
+ method_name = camel_case2snake_case(r.method)
57
+
58
+ if method_name not in result:
59
+ result[method_name] = CallsList()
60
+
61
+ result[method_name].append(self._dict_to_obj(r.data))
62
+
63
+ return self._generate_result_obj(result)
64
+
65
+ def add_result_for(
66
+ self,
67
+ method: type[TelegramMethod[TelegramType]],
68
+ ok: bool,
69
+ result: TelegramType = None,
70
+ description: str | None = None,
71
+ error_code: int = 200,
72
+ migrate_to_chat_id: int | None = None,
73
+ retry_after: int | None = None,
74
+ ) -> Response[TelegramType]:
75
+ response = self._handler.add_result_for(
76
+ method=method,
77
+ ok=ok,
78
+ result=result,
79
+ description=description,
80
+ error_code=error_code,
81
+ migrate_to_chat_id=migrate_to_chat_id,
82
+ retry_after=retry_after,
83
+ )
84
+ return response
85
+
86
+ @staticmethod
87
+ def _dict_to_obj(data: dict):
88
+ GeneratedResponse = type("GeneratedResponse", (), data)
89
+ return GeneratedResponse()
90
+
91
+ @staticmethod
92
+ def _generate_result_obj(data: dict):
93
+ GeneratedCalls = type("GeneratedCalls", (Calls,), data)
94
+ return GeneratedCalls()
@@ -0,0 +1,3 @@
1
+ from . import dataset
2
+
3
+ __all__ = ("dataset",)
@@ -0,0 +1,673 @@
1
+ from aiogram import types
2
+
3
+ from .base import DatasetItem
4
+
5
+ USER = DatasetItem(
6
+ {
7
+ "id": 12345678,
8
+ "is_bot": False,
9
+ "first_name": "FirstName",
10
+ "last_name": "LastName",
11
+ "username": "username",
12
+ "language_code": "ru",
13
+ },
14
+ model=types.User,
15
+ )
16
+
17
+ CHAT = DatasetItem(
18
+ {
19
+ "id": 12345678,
20
+ "first_name": "FirstName",
21
+ "last_name": "LastName",
22
+ "username": "username",
23
+ "type": "private",
24
+ },
25
+ model=types.Chat,
26
+ )
27
+
28
+ CHAT_PHOTO = DatasetItem(
29
+ {
30
+ "small_file_id": "small_file_id",
31
+ "small_file_unique_id": "small_file_unique_id",
32
+ "big_file_id": "big_file_id",
33
+ "big_file_unique_id": "big_file_unique_id",
34
+ },
35
+ model=types.ChatPhoto,
36
+ )
37
+
38
+ PHOTO = DatasetItem(
39
+ {
40
+ "file_id": "AgADBAADFak0G88YZAf8OAug7bHyS9x2ZxkABHVfpJywcloRAAGAAQABAg",
41
+ "file_unique_id": "file_unique_id",
42
+ "file_size": 1101,
43
+ "width": 90,
44
+ "height": 51,
45
+ },
46
+ model=types.PhotoSize,
47
+ )
48
+
49
+ AUDIO = DatasetItem(
50
+ {
51
+ "duration": 236,
52
+ "mime_type": "audio/mpeg3",
53
+ "title": "The Best Song",
54
+ "performer": "The Best Singer",
55
+ "file_id": "CQADAgADbQEAAsnrIUpNoRRNsH7_hAI",
56
+ "file_size": 9507774,
57
+ "file_unique_id": "file_unique_id",
58
+ },
59
+ model=types.Audio,
60
+ )
61
+
62
+ BOT_COMMAND = DatasetItem(
63
+ {
64
+ "command": "start",
65
+ "description": "Start bot",
66
+ },
67
+ model=types.BotCommand,
68
+ )
69
+
70
+ CHAT_MEMBER = DatasetItem(
71
+ {
72
+ "user": USER,
73
+ "status": "administrator",
74
+ "can_be_edited": False,
75
+ "can_manage_chat": True,
76
+ "can_change_info": True,
77
+ "can_delete_messages": True,
78
+ "can_invite_users": True,
79
+ "can_restrict_members": True,
80
+ "can_pin_messages": True,
81
+ "can_promote_members": False,
82
+ "can_manage_voice_chats": True, # Deprecated
83
+ "can_manage_video_chats": True,
84
+ "is_anonymous": False,
85
+ },
86
+ model=types.ChatMember,
87
+ )
88
+
89
+ CHAT_MEMBER_OWNER = DatasetItem(
90
+ {
91
+ "user": USER,
92
+ "status": "creator",
93
+ "is_anonymous": False,
94
+ },
95
+ model=types.ChatMemberOwner,
96
+ )
97
+
98
+ CONTACT = DatasetItem(
99
+ {
100
+ "phone_number": "88005553535",
101
+ "first_name": "John",
102
+ "last_name": "Smith",
103
+ },
104
+ model=types.Contact,
105
+ )
106
+
107
+ DICE = DatasetItem({"value": 6, "emoji": "🎲"}, model=types.Dice)
108
+
109
+ DOCUMENT = DatasetItem(
110
+ {
111
+ "file_name": "test.docx",
112
+ "mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
113
+ "file_id": "BQADAgADpgADy_JxS66XQTBRHFleAg",
114
+ "file_unique_id": "file_unique_id",
115
+ "file_size": 21331,
116
+ },
117
+ model=types.Document,
118
+ )
119
+
120
+ ANIMATION = DatasetItem(
121
+ {"file_id": "file_id", "file_unique_id": "file_unique_id", "width": 50, "height": 50, "duration": 50},
122
+ model=types.Animation,
123
+ )
124
+
125
+ ENTITY_BOLD = DatasetItem(
126
+ {
127
+ "offset": 5,
128
+ "length": 2,
129
+ "type": "bold",
130
+ }
131
+ )
132
+
133
+ ENTITY_ITALIC = DatasetItem(
134
+ {
135
+ "offset": 8,
136
+ "length": 1,
137
+ "type": "italic",
138
+ }
139
+ )
140
+
141
+ ENTITY_LINK = DatasetItem(
142
+ {
143
+ "offset": 10,
144
+ "length": 6,
145
+ "type": "text_link",
146
+ "url": "https://google.com/",
147
+ }
148
+ )
149
+
150
+ ENTITY_CODE = DatasetItem(
151
+ {
152
+ "offset": 17,
153
+ "length": 7,
154
+ "type": "code",
155
+ }
156
+ )
157
+
158
+ ENTITY_PRE = DatasetItem(
159
+ {
160
+ "offset": 30,
161
+ "length": 4,
162
+ "type": "pre",
163
+ }
164
+ )
165
+
166
+ ENTITY_MENTION = DatasetItem(
167
+ {
168
+ "offset": 47,
169
+ "length": 9,
170
+ "type": "mention",
171
+ }
172
+ )
173
+
174
+ GAME = DatasetItem(
175
+ {
176
+ "title": "Karate Kido",
177
+ "description": "No trees were harmed in the making of this game :)",
178
+ "photo": [PHOTO, PHOTO, PHOTO],
179
+ "animation": ANIMATION,
180
+ },
181
+ model=types.Game,
182
+ )
183
+
184
+ INVOICE = DatasetItem(
185
+ {
186
+ "title": "Working Time Machine",
187
+ "description": "Want to visit your great-great-great-grandparents? "
188
+ "Make a fortune at the races? "
189
+ "Shake hands with Hammurabi and take a stroll in the Hanging Gardens? "
190
+ "Order our Working Time Machine today!",
191
+ "start_parameter": "time-machine-example",
192
+ "currency": "USD",
193
+ "total_amount": 6250,
194
+ },
195
+ model=types.Invoice,
196
+ )
197
+
198
+ LOCATION = DatasetItem(
199
+ {
200
+ "latitude": 50.693416,
201
+ "longitude": 30.624605,
202
+ },
203
+ model=types.Location,
204
+ )
205
+
206
+ VENUE = DatasetItem(
207
+ {
208
+ "location": LOCATION,
209
+ "title": "Venue Name",
210
+ "address": "Venue Address",
211
+ "foursquare_id": "4e6f2cec483bad563d150f98",
212
+ },
213
+ model=types.Venue,
214
+ )
215
+
216
+ SHIPPING_ADDRESS = DatasetItem(
217
+ {
218
+ "country_code": "US",
219
+ "state": "State",
220
+ "city": "DefaultCity",
221
+ "street_line1": "Central",
222
+ "street_line2": "Middle",
223
+ "post_code": "424242",
224
+ },
225
+ model=types.ShippingAddress,
226
+ )
227
+
228
+ STICKER = DatasetItem(
229
+ {
230
+ "width": 512,
231
+ "height": 512,
232
+ "emoji": "🛠",
233
+ "set_name": "StickerSet",
234
+ "thumb": PHOTO,
235
+ "file_id": "AAbbCCddEEffGGhh1234567890",
236
+ "file_size": 12345,
237
+ "file_unique_id": "file_unique_id",
238
+ "type": "type",
239
+ "is_animated": False,
240
+ "is_video": False,
241
+ },
242
+ model=types.Sticker,
243
+ )
244
+
245
+ SUCCESSFUL_PAYMENT = DatasetItem(
246
+ {
247
+ "currency": "USD",
248
+ "total_amount": 6250,
249
+ "invoice_payload": "HAPPY FRIDAYS COUPON",
250
+ "telegram_payment_charge_id": "_",
251
+ "provider_payment_charge_id": "12345678901234_test",
252
+ },
253
+ model=types.SuccessfulPayment,
254
+ )
255
+
256
+ VIDEO = DatasetItem(
257
+ {
258
+ "duration": 52,
259
+ "width": 853,
260
+ "height": 480,
261
+ "mime_type": "video/quicktime",
262
+ "thumb": PHOTO,
263
+ "file_id": "BAADAgpAADdawy_JxS72kRvV3cortAg",
264
+ "file_unique_id": "file_unique_id",
265
+ "file_size": 10099782,
266
+ },
267
+ model=types.Video,
268
+ )
269
+
270
+ VIDEO_NOTE = DatasetItem(
271
+ {
272
+ "duration": 4,
273
+ "length": 240,
274
+ "thumb": PHOTO,
275
+ "file_id": "AbCdEfGhIjKlMnOpQrStUvWxYz",
276
+ "file_unique_id": "file_unique_id",
277
+ "file_size": 186562,
278
+ },
279
+ model=types.VideoNote,
280
+ )
281
+
282
+ VOICE = DatasetItem(
283
+ {
284
+ "duration": 1,
285
+ "mime_type": "audio/ogg",
286
+ "file_id": "AwADawAgADADy_JxS2gopIVIIxlhAg",
287
+ "file_unique_id": "file_unique_id",
288
+ "file_size": 4321,
289
+ },
290
+ model=types.Voice,
291
+ )
292
+
293
+ MESSAGE = DatasetItem(
294
+ {
295
+ "message_id": 11223,
296
+ "from": USER,
297
+ "chat": CHAT,
298
+ "date": 1508709711,
299
+ "text": "Hi, world!",
300
+ },
301
+ model=types.Message,
302
+ )
303
+
304
+ CALLBACK_QUERY = DatasetItem(
305
+ {"id": "12345678", "chat_instance": "AABBCC", "from_user": USER, "message": MESSAGE, "data": "data"},
306
+ model=types.CallbackQuery,
307
+ )
308
+
309
+ CHANNEL = DatasetItem(
310
+ {
311
+ "type": "channel",
312
+ "username": "best_channel_ever",
313
+ "id": -1001065170817,
314
+ },
315
+ model=types.Chat,
316
+ )
317
+
318
+ CHANNEL_POST = DatasetItem(
319
+ {"message_id": 12345, "sender_chat": CHANNEL, "chat": CHANNEL, "date": 1508825372, "text": "Hi, channel!"},
320
+ model=types.Message,
321
+ )
322
+
323
+ EDITED_CHANNEL_POST = DatasetItem(
324
+ {
325
+ "message_id": 12345,
326
+ "sender_chat": CHANNEL,
327
+ "chat": CHANNEL,
328
+ "date": 1508825372,
329
+ "edit_date": 1508825379,
330
+ "text": "Hi, channel! (edited)",
331
+ },
332
+ model=types.Message,
333
+ )
334
+
335
+ EDITED_MESSAGE = DatasetItem(
336
+ {
337
+ "message_id": 12345,
338
+ "from": USER,
339
+ "chat": CHAT,
340
+ "date": 1508825372,
341
+ "edit_date": 1508825379,
342
+ "text": "hi there (edited)",
343
+ },
344
+ model=types.Message,
345
+ )
346
+
347
+ FORWARDED_MESSAGE = DatasetItem(
348
+ {
349
+ "message_id": 12345,
350
+ "from": USER,
351
+ "chat": CHAT,
352
+ "date": 1522828529,
353
+ "forward_from_chat": CHAT,
354
+ "forward_from_message_id": 123,
355
+ "forward_date": 1522749037,
356
+ "text": "Forwarded text with entities from public channel ",
357
+ "entities": [ENTITY_BOLD, ENTITY_CODE, ENTITY_ITALIC, ENTITY_LINK, ENTITY_LINK, ENTITY_MENTION, ENTITY_PRE],
358
+ },
359
+ model=types.Message,
360
+ )
361
+
362
+ MESSAGE_WITH_AUDIO = DatasetItem(
363
+ {
364
+ "message_id": 12345,
365
+ "from": USER,
366
+ "chat": CHAT,
367
+ "date": 1508739776,
368
+ "audio": AUDIO,
369
+ "caption": "This is my favourite song",
370
+ },
371
+ model=types.Message,
372
+ )
373
+
374
+ MESSAGE_WITH_CONTACT = DatasetItem(
375
+ {
376
+ "message_id": 56006,
377
+ "from": USER,
378
+ "chat": CHAT,
379
+ "date": 1522850298,
380
+ "contact": CONTACT,
381
+ },
382
+ model=types.Message,
383
+ )
384
+
385
+ MESSAGE_WITH_DICE = DatasetItem(
386
+ {"message_id": 12345, "from": USER, "chat": CHAT, "date": 1508768012, "dice": DICE}, model=types.Message
387
+ )
388
+
389
+ MESSAGE_WITH_DOCUMENT = DatasetItem(
390
+ {
391
+ "message_id": 12345,
392
+ "from": USER,
393
+ "chat": CHAT,
394
+ "date": 1508768012,
395
+ "document": DOCUMENT,
396
+ "caption": "Read my document",
397
+ },
398
+ model=types.Message,
399
+ )
400
+
401
+ MESSAGE_WITH_GAME = DatasetItem(
402
+ {
403
+ "message_id": 12345,
404
+ "from": USER,
405
+ "chat": CHAT,
406
+ "date": 1508824810,
407
+ "game": GAME,
408
+ },
409
+ model=types.Message,
410
+ )
411
+
412
+ MESSAGE_WITH_INVOICE = DatasetItem(
413
+ {
414
+ "message_id": 9772,
415
+ "from": USER,
416
+ "chat": CHAT,
417
+ "date": 1508761719,
418
+ "invoice": INVOICE,
419
+ },
420
+ model=types.Message,
421
+ )
422
+
423
+ MESSAGE_WITH_LOCATION = DatasetItem(
424
+ {
425
+ "message_id": 12345,
426
+ "from": USER,
427
+ "chat": CHAT,
428
+ "date": 1508755473,
429
+ "location": LOCATION,
430
+ },
431
+ model=types.Message,
432
+ )
433
+
434
+ MESSAGE_WITH_MIGRATE_TO_CHAT_ID = DatasetItem(
435
+ {
436
+ "message_id": 12345,
437
+ "from": USER,
438
+ "chat": CHAT,
439
+ "date": 1526943253,
440
+ "migrate_to_chat_id": -1234567890987,
441
+ },
442
+ model=types.Message,
443
+ )
444
+
445
+ MESSAGE_WITH_MIGRATE_FROM_CHAT_ID = DatasetItem(
446
+ {
447
+ "message_id": 12345,
448
+ "from": USER,
449
+ "chat": CHAT,
450
+ "date": 1526943253,
451
+ "migrate_from_chat_id": -123456789,
452
+ },
453
+ model=types.Message,
454
+ )
455
+
456
+ MESSAGE_WITH_PHOTO = DatasetItem(
457
+ {
458
+ "message_id": 12345,
459
+ "from": USER,
460
+ "chat": CHAT,
461
+ "date": 1508825154,
462
+ "photo": [PHOTO, PHOTO, PHOTO, PHOTO],
463
+ "caption": "photo description",
464
+ },
465
+ model=types.Message,
466
+ )
467
+
468
+ MESSAGE_WITH_MEDIA_GROUP = DatasetItem(
469
+ {
470
+ "message_id": 55966,
471
+ "from": USER,
472
+ "chat": CHAT,
473
+ "date": 1522843665,
474
+ "media_group_id": "12182749320567362",
475
+ "photo": [PHOTO, PHOTO, PHOTO, PHOTO],
476
+ },
477
+ model=types.Message,
478
+ )
479
+
480
+ MESSAGE_WITH_STICKER = DatasetItem(
481
+ {
482
+ "message_id": 12345,
483
+ "from": USER,
484
+ "chat": CHAT,
485
+ "date": 1508771450,
486
+ "sticker": STICKER,
487
+ },
488
+ model=types.Message,
489
+ )
490
+
491
+ MESSAGE_WITH_SUCCESSFUL_PAYMENT = DatasetItem(
492
+ {
493
+ "message_id": 9768,
494
+ "from": USER,
495
+ "chat": CHAT,
496
+ "date": 1508761169,
497
+ "successful_payment": SUCCESSFUL_PAYMENT,
498
+ },
499
+ model=types.Message,
500
+ )
501
+
502
+ MESSAGE_WITH_VENUE = DatasetItem(
503
+ {
504
+ "message_id": 56004,
505
+ "from": USER,
506
+ "chat": CHAT,
507
+ "date": 1522849819,
508
+ "location": LOCATION,
509
+ "venue": VENUE,
510
+ },
511
+ model=types.Message,
512
+ )
513
+
514
+ MESSAGE_WITH_VIDEO = DatasetItem(
515
+ {
516
+ "message_id": 12345,
517
+ "from": USER,
518
+ "chat": CHAT,
519
+ "date": 1508756494,
520
+ "video": VIDEO,
521
+ "caption": "description",
522
+ },
523
+ model=types.Message,
524
+ )
525
+
526
+ MESSAGE_WITH_VIDEO_NOTE = DatasetItem(
527
+ {
528
+ "message_id": 55934,
529
+ "from": USER,
530
+ "chat": CHAT,
531
+ "date": 1522835890,
532
+ "video_note": VIDEO_NOTE,
533
+ },
534
+ model=types.Message,
535
+ )
536
+
537
+ MESSAGE_WITH_VOICE = DatasetItem(
538
+ {
539
+ "message_id": 12345,
540
+ "from": USER,
541
+ "chat": CHAT,
542
+ "date": 1508768403,
543
+ "voice": VOICE,
544
+ },
545
+ model=types.Message,
546
+ )
547
+
548
+ MESSAGE_FROM_CHANNEL = DatasetItem(
549
+ {
550
+ "message_id": 123432,
551
+ "from": None,
552
+ "chat": CHANNEL,
553
+ "date": 1508768405,
554
+ "text": "Hi, world!",
555
+ },
556
+ model=types.Message,
557
+ )
558
+
559
+ PRE_CHECKOUT_QUERY = DatasetItem(
560
+ {
561
+ "id": "262181558630368727",
562
+ "from": USER,
563
+ "currency": "USD",
564
+ "total_amount": 6250,
565
+ "invoice_payload": "HAPPY FRIDAYS COUPON",
566
+ },
567
+ model=types.PreCheckoutQuery,
568
+ )
569
+
570
+ REPLY_MESSAGE = DatasetItem(
571
+ {
572
+ "message_id": 12345,
573
+ "from": USER,
574
+ "chat": CHAT,
575
+ "date": 1508751866,
576
+ "reply_to_message": MESSAGE,
577
+ "text": "Reply to quoted message",
578
+ },
579
+ model=types.Message,
580
+ )
581
+
582
+ SHIPPING_QUERY = DatasetItem(
583
+ {
584
+ "id": "262181558684397422",
585
+ "from": USER,
586
+ "invoice_payload": "HAPPY FRIDAYS COUPON",
587
+ "shipping_address": SHIPPING_ADDRESS,
588
+ },
589
+ model=types.ShippingQuery,
590
+ )
591
+
592
+ USER_PROFILE_PHOTOS = DatasetItem(
593
+ {
594
+ "total_count": 1,
595
+ "photos": [
596
+ [PHOTO, PHOTO, PHOTO],
597
+ ],
598
+ },
599
+ model=types.UserProfilePhotos,
600
+ )
601
+
602
+ FILE = DatasetItem(
603
+ {"file_id": "XXXYYYZZZ", "file_size": 5254, "file_path": "voice/file_8", "file_unique_id": "file_unique_id"},
604
+ model=types.File,
605
+ )
606
+
607
+ UPDATE = DatasetItem(
608
+ {
609
+ "update_id": 123456789,
610
+ "message": MESSAGE,
611
+ },
612
+ model=types.Update,
613
+ )
614
+
615
+ WEBHOOK_INFO = DatasetItem(
616
+ {
617
+ "url": "",
618
+ "has_custom_certificate": False,
619
+ "pending_update_count": 0,
620
+ },
621
+ model=types.WebhookInfo,
622
+ )
623
+
624
+ REPLY_KEYBOARD_MARKUP = DatasetItem(
625
+ {
626
+ "keyboard": [[{"text": "something here"}]],
627
+ "resize_keyboard": True,
628
+ },
629
+ model=types.ReplyKeyboardMarkup,
630
+ )
631
+
632
+ CHAT_PERMISSIONS = DatasetItem(
633
+ {
634
+ "can_send_messages": True,
635
+ "can_send_media_messages": True,
636
+ "can_send_polls": True,
637
+ "can_send_other_messages": True,
638
+ "can_add_web_page_previews": True,
639
+ "can_change_info": True,
640
+ "can_invite_users": True,
641
+ "can_pin_messages": True,
642
+ },
643
+ model=types.ChatPermissions,
644
+ )
645
+
646
+ CHAT_LOCATION = DatasetItem(
647
+ {
648
+ "location": LOCATION,
649
+ "address": "address",
650
+ },
651
+ model=types.ChatLocation,
652
+ )
653
+
654
+ FULL_CHAT = DatasetItem(
655
+ {
656
+ **CHAT,
657
+ "photo": CHAT_PHOTO,
658
+ "bio": "bio",
659
+ "has_private_forwards": False,
660
+ "description": "description",
661
+ "invite_link": "invite_link",
662
+ "pinned_message": MESSAGE,
663
+ "permissions": CHAT_PERMISSIONS,
664
+ "slow_mode_delay": 10,
665
+ "message_auto_delete_time": 60,
666
+ "has_protected_content": True,
667
+ "sticker_set_name": "sticker_set_name",
668
+ "can_set_sticker_set": True,
669
+ "linked_chat_id": -1234567890,
670
+ "location": CHAT_LOCATION,
671
+ },
672
+ model=types.Chat,
673
+ )
@@ -0,0 +1,68 @@
1
+ from collections.abc import Mapping
2
+ from typing import Any
3
+
4
+
5
+ class DatasetItem(Mapping):
6
+ def __init__(self, data: dict, *, model=None, name=None):
7
+ self._data = data
8
+ self._name = name
9
+ self._model = model
10
+
11
+ @property
12
+ def data(self) -> dict:
13
+ return self._data
14
+
15
+ @property
16
+ def name(self) -> str:
17
+ return self._name
18
+
19
+ @property
20
+ def model(self) -> Any:
21
+ return self._model
22
+
23
+ def as_object(self, **replace_args) -> Any | None:
24
+ """
25
+ Return an object from dict
26
+
27
+ :return: Any | None
28
+ """
29
+ try:
30
+ data = self._data.copy()
31
+ data.update(**replace_args)
32
+ if self._model and isinstance(self._data, dict):
33
+ return self._recursive_as_object(data, self._model)
34
+ else:
35
+ return data
36
+ except (AttributeError, TypeError):
37
+ return None
38
+
39
+ def _recursive_as_object(self, data: dict, model: Any):
40
+ """
41
+ This method is converting dict data to object, if one of the params is the DatasetItem method will be
42
+ recursive convert it;
43
+
44
+ :param data: the dict that should be as object
45
+ :param model: the object that will be returned
46
+ :return:
47
+ """
48
+ result_data = data.copy()
49
+ for key, value in data.items():
50
+ if isinstance(value, DatasetItem):
51
+ result_data[key] = self._recursive_as_object(value.data, value.model)
52
+ elif isinstance(value, list):
53
+ for index, item in enumerate(value):
54
+ if not isinstance(item, (DatasetItem, list)):
55
+ continue
56
+
57
+ result_data[key][index] = self._recursive_as_object(item.data, item.model)
58
+
59
+ return model(**result_data)
60
+
61
+ def __iter__(self):
62
+ return iter(self._data.keys())
63
+
64
+ def __getitem__(self, item):
65
+ return self._data[item]
66
+
67
+ def __len__(self):
68
+ return len(self._data)
aiogram_tests/utils.py ADDED
@@ -0,0 +1,5 @@
1
+ import re
2
+
3
+
4
+ def camel_case2snake_case(var: str) -> str:
5
+ return re.sub(r"(?<!^)(?=[A-Z])", "_", var).lower()