telegrinder 0.1.dev170__py3-none-any.whl → 0.2.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.

Potentially problematic release.


This version of telegrinder might be problematic. Click here for more details.

Files changed (116) hide show
  1. telegrinder/__init__.py +2 -2
  2. telegrinder/api/__init__.py +1 -2
  3. telegrinder/api/api.py +15 -6
  4. telegrinder/api/error.py +2 -1
  5. telegrinder/api/token.py +36 -0
  6. telegrinder/bot/__init__.py +12 -6
  7. telegrinder/bot/bot.py +18 -6
  8. telegrinder/bot/cute_types/__init__.py +7 -7
  9. telegrinder/bot/cute_types/base.py +122 -20
  10. telegrinder/bot/cute_types/callback_query.py +10 -6
  11. telegrinder/bot/cute_types/chat_join_request.py +4 -5
  12. telegrinder/bot/cute_types/chat_member_updated.py +4 -6
  13. telegrinder/bot/cute_types/inline_query.py +3 -4
  14. telegrinder/bot/cute_types/message.py +32 -21
  15. telegrinder/bot/cute_types/update.py +51 -4
  16. telegrinder/bot/cute_types/utils.py +3 -466
  17. telegrinder/bot/dispatch/__init__.py +10 -11
  18. telegrinder/bot/dispatch/abc.py +8 -5
  19. telegrinder/bot/dispatch/context.py +17 -8
  20. telegrinder/bot/dispatch/dispatch.py +71 -48
  21. telegrinder/bot/dispatch/handler/__init__.py +3 -3
  22. telegrinder/bot/dispatch/handler/abc.py +4 -4
  23. telegrinder/bot/dispatch/handler/func.py +46 -22
  24. telegrinder/bot/dispatch/handler/message_reply.py +6 -7
  25. telegrinder/bot/dispatch/middleware/__init__.py +1 -1
  26. telegrinder/bot/dispatch/middleware/abc.py +2 -2
  27. telegrinder/bot/dispatch/process.py +38 -19
  28. telegrinder/bot/dispatch/return_manager/__init__.py +4 -4
  29. telegrinder/bot/dispatch/return_manager/abc.py +3 -3
  30. telegrinder/bot/dispatch/return_manager/callback_query.py +1 -2
  31. telegrinder/bot/dispatch/return_manager/inline_query.py +1 -2
  32. telegrinder/bot/dispatch/return_manager/message.py +1 -2
  33. telegrinder/bot/dispatch/view/__init__.py +8 -8
  34. telegrinder/bot/dispatch/view/abc.py +18 -16
  35. telegrinder/bot/dispatch/view/box.py +75 -64
  36. telegrinder/bot/dispatch/view/callback_query.py +1 -2
  37. telegrinder/bot/dispatch/view/chat_join_request.py +1 -2
  38. telegrinder/bot/dispatch/view/chat_member.py +16 -2
  39. telegrinder/bot/dispatch/view/inline_query.py +1 -2
  40. telegrinder/bot/dispatch/view/message.py +12 -5
  41. telegrinder/bot/dispatch/view/raw.py +9 -8
  42. telegrinder/bot/dispatch/waiter_machine/__init__.py +3 -3
  43. telegrinder/bot/dispatch/waiter_machine/machine.py +12 -8
  44. telegrinder/bot/dispatch/waiter_machine/middleware.py +1 -1
  45. telegrinder/bot/dispatch/waiter_machine/short_state.py +4 -3
  46. telegrinder/bot/polling/abc.py +1 -1
  47. telegrinder/bot/polling/polling.py +6 -6
  48. telegrinder/bot/rules/__init__.py +20 -20
  49. telegrinder/bot/rules/abc.py +57 -43
  50. telegrinder/bot/rules/adapter/__init__.py +5 -5
  51. telegrinder/bot/rules/adapter/abc.py +6 -3
  52. telegrinder/bot/rules/adapter/errors.py +2 -1
  53. telegrinder/bot/rules/adapter/event.py +28 -13
  54. telegrinder/bot/rules/adapter/node.py +28 -22
  55. telegrinder/bot/rules/adapter/raw_update.py +13 -5
  56. telegrinder/bot/rules/callback_data.py +4 -4
  57. telegrinder/bot/rules/chat_join.py +4 -4
  58. telegrinder/bot/rules/command.py +5 -7
  59. telegrinder/bot/rules/func.py +2 -2
  60. telegrinder/bot/rules/fuzzy.py +1 -1
  61. telegrinder/bot/rules/inline.py +3 -3
  62. telegrinder/bot/rules/integer.py +1 -2
  63. telegrinder/bot/rules/markup.py +5 -3
  64. telegrinder/bot/rules/message_entities.py +2 -2
  65. telegrinder/bot/rules/node.py +2 -2
  66. telegrinder/bot/rules/regex.py +1 -1
  67. telegrinder/bot/rules/rule_enum.py +1 -1
  68. telegrinder/bot/rules/text.py +1 -2
  69. telegrinder/bot/rules/update.py +1 -2
  70. telegrinder/bot/scenario/abc.py +2 -2
  71. telegrinder/bot/scenario/checkbox.py +3 -4
  72. telegrinder/bot/scenario/choice.py +1 -2
  73. telegrinder/model.py +89 -45
  74. telegrinder/modules.py +3 -3
  75. telegrinder/msgspec_utils.py +85 -57
  76. telegrinder/node/__init__.py +17 -10
  77. telegrinder/node/attachment.py +19 -16
  78. telegrinder/node/base.py +46 -22
  79. telegrinder/node/callback_query.py +5 -9
  80. telegrinder/node/command.py +6 -2
  81. telegrinder/node/composer.py +102 -77
  82. telegrinder/node/container.py +3 -3
  83. telegrinder/node/event.py +68 -0
  84. telegrinder/node/me.py +3 -0
  85. telegrinder/node/message.py +6 -10
  86. telegrinder/node/polymorphic.py +15 -10
  87. telegrinder/node/rule.py +20 -6
  88. telegrinder/node/scope.py +9 -1
  89. telegrinder/node/source.py +21 -11
  90. telegrinder/node/text.py +4 -4
  91. telegrinder/node/update.py +7 -4
  92. telegrinder/py.typed +0 -0
  93. telegrinder/rules.py +59 -0
  94. telegrinder/tools/__init__.py +2 -2
  95. telegrinder/tools/buttons.py +5 -10
  96. telegrinder/tools/error_handler/abc.py +2 -2
  97. telegrinder/tools/error_handler/error.py +2 -0
  98. telegrinder/tools/error_handler/error_handler.py +6 -6
  99. telegrinder/tools/formatting/spec_html_formats.py +10 -10
  100. telegrinder/tools/global_context/__init__.py +2 -2
  101. telegrinder/tools/global_context/global_context.py +3 -3
  102. telegrinder/tools/global_context/telegrinder_ctx.py +4 -4
  103. telegrinder/tools/keyboard.py +3 -3
  104. telegrinder/tools/loop_wrapper/loop_wrapper.py +47 -13
  105. telegrinder/tools/magic.py +96 -18
  106. telegrinder/types/__init__.py +1 -0
  107. telegrinder/types/enums.py +2 -0
  108. telegrinder/types/methods.py +91 -15
  109. telegrinder/types/objects.py +49 -24
  110. telegrinder/verification_utils.py +1 -3
  111. {telegrinder-0.1.dev170.dist-info → telegrinder-0.2.0.dist-info}/METADATA +2 -2
  112. telegrinder-0.2.0.dist-info/RECORD +145 -0
  113. telegrinder/api/abc.py +0 -73
  114. telegrinder-0.1.dev170.dist-info/RECORD +0 -143
  115. {telegrinder-0.1.dev170.dist-info → telegrinder-0.2.0.dist-info}/LICENSE +0 -0
  116. {telegrinder-0.1.dev170.dist-info → telegrinder-0.2.0.dist-info}/WHEEL +0 -0
telegrinder/__init__.py CHANGED
@@ -34,7 +34,7 @@ bot.run_forever()
34
34
 
35
35
  import typing
36
36
 
37
- from .api import ABCAPI, API, APIError, APIResponse, Token
37
+ from .api import API, APIError, APIResponse, Token
38
38
  from .bot import (
39
39
  ABCDispatch,
40
40
  ABCHandler,
@@ -122,7 +122,7 @@ Bot: typing.TypeAlias = Telegrinder
122
122
 
123
123
 
124
124
  __all__ = (
125
- "ABCAPI",
125
+ "API",
126
126
  "ABCClient",
127
127
  "ABCDispatch",
128
128
  "ABCErrorHandler",
@@ -1,10 +1,9 @@
1
- from .abc import ABCAPI, Token
2
1
  from .api import API
3
2
  from .error import APIError, InvalidTokenError
4
3
  from .response import APIResponse
4
+ from .token import Token
5
5
 
6
6
  __all__ = (
7
- "ABCAPI",
8
7
  "API",
9
8
  "APIError",
10
9
  "APIResponse",
telegrinder/api/api.py CHANGED
@@ -1,32 +1,34 @@
1
1
  import typing
2
+ from functools import cached_property
2
3
 
3
4
  import msgspec
4
5
  from fntypes.result import Error, Ok, Result
5
6
 
7
+ from telegrinder.api.error import APIError
6
8
  from telegrinder.api.response import APIResponse
9
+ from telegrinder.api.token import Token
7
10
  from telegrinder.client import ABCClient, AiohttpClient
8
11
  from telegrinder.model import DataConverter, decoder
9
12
  from telegrinder.types.methods import APIMethods
10
13
 
11
- from .abc import ABCAPI, APIError, Token
12
-
13
14
 
14
15
  def compose_data(
15
16
  client: ABCClient,
16
17
  data: dict[str, typing.Any],
17
18
  files: dict[str, tuple[str, bytes]],
18
19
  ) -> typing.Any:
19
- converter = DataConverter(files=files.copy())
20
+ converter = DataConverter(_files=files.copy())
20
21
  return client.get_form(
21
22
  data={k: converter(v) for k, v in data.items()},
22
23
  files=converter.files,
23
24
  )
24
25
 
25
26
 
26
- class API(ABCAPI, APIMethods):
27
- """Bot API with available API methods."""
27
+ class API(APIMethods):
28
+ """Bot API with available API methods and http client."""
28
29
 
29
30
  API_URL = "https://api.telegram.org/"
31
+ API_FILE_URL = "https://api.telegram.org/file/"
30
32
 
31
33
  def __init__(self, token: Token, *, http: ABCClient | None = None) -> None:
32
34
  self.token = token
@@ -40,7 +42,7 @@ class API(ABCAPI, APIMethods):
40
42
  self.http,
41
43
  )
42
44
 
43
- @property
45
+ @cached_property
44
46
  def id(self) -> int:
45
47
  return self.token.bot_id
46
48
 
@@ -48,6 +50,13 @@ class API(ABCAPI, APIMethods):
48
50
  def request_url(self) -> str:
49
51
  return self.API_URL + f"bot{self.token}/"
50
52
 
53
+ @property
54
+ def request_file_url(self) -> str:
55
+ return self.API_FILE_URL + f"bot{self.token}/"
56
+
57
+ async def download_file(self, file_path: str) -> bytes:
58
+ return await self.http.request_content(f"{self.request_file_url}/{file_path}")
59
+
51
60
  async def request(
52
61
  self,
53
62
  method: str,
telegrinder/api/error.py CHANGED
@@ -9,7 +9,8 @@ class APIError(BaseException):
9
9
  return f"<APIError: {self.__str__()}>"
10
10
 
11
11
 
12
- class InvalidTokenError(BaseException): ...
12
+ class InvalidTokenError(BaseException):
13
+ pass
13
14
 
14
15
 
15
16
  __all__ = ("APIError", "InvalidTokenError")
@@ -0,0 +1,36 @@
1
+ import pathlib
2
+ import typing
3
+ from functools import cached_property
4
+
5
+ from envparse import env
6
+
7
+ from .error import InvalidTokenError
8
+
9
+
10
+ class Token(str):
11
+ def __new__(cls, token: str) -> typing.Self:
12
+ if token.count(":") != 1 or not token.split(":")[0].isdigit():
13
+ raise InvalidTokenError("Invalid token, it should look like this '123:ABC'.")
14
+ return super().__new__(cls, token)
15
+
16
+ def __repr__(self) -> str:
17
+ return f"<Token: {self.bot_id}:{''.join(self.split(':')[-1])[:6]}...>"
18
+
19
+ @classmethod
20
+ def from_env(
21
+ cls,
22
+ var_name: str = "BOT_TOKEN",
23
+ *,
24
+ is_read: bool = False,
25
+ path_to_envfile: str | pathlib.Path | None = None,
26
+ ) -> typing.Self:
27
+ if not is_read:
28
+ env.read_envfile(path_to_envfile)
29
+ return cls(env.str(var_name))
30
+
31
+ @cached_property
32
+ def bot_id(self) -> int:
33
+ return int(self.split(":")[0])
34
+
35
+
36
+ __all__ = ("Token",)
@@ -1,5 +1,5 @@
1
- from .bot import Telegrinder
2
- from .cute_types import (
1
+ from telegrinder.bot.bot import Telegrinder
2
+ from telegrinder.bot.cute_types import (
3
3
  BaseCute,
4
4
  CallbackQueryCute,
5
5
  ChatJoinRequestCute,
@@ -8,7 +8,7 @@ from .cute_types import (
8
8
  MessageCute,
9
9
  UpdateCute,
10
10
  )
11
- from .dispatch import (
11
+ from telegrinder.bot.dispatch import (
12
12
  ABCDispatch,
13
13
  ABCHandler,
14
14
  ABCMiddleware,
@@ -37,9 +37,15 @@ from .dispatch import (
37
37
  clear_wm_storage_worker,
38
38
  register_manager,
39
39
  )
40
- from .polling import ABCPolling, Polling
41
- from .rules import ABCRule, CallbackQueryRule, ChatJoinRequestRule, InlineQueryRule, MessageRule
42
- from .scenario import ABCScenario, Checkbox, Choice
40
+ from telegrinder.bot.polling import ABCPolling, Polling
41
+ from telegrinder.bot.rules import (
42
+ ABCRule,
43
+ CallbackQueryRule,
44
+ ChatJoinRequestRule,
45
+ InlineQueryRule,
46
+ MessageRule,
47
+ )
48
+ from telegrinder.bot.scenario import ABCScenario, Checkbox, Choice
43
49
 
44
50
  __all__ = (
45
51
  "ABCDispatch",
telegrinder/bot/bot.py CHANGED
@@ -1,10 +1,13 @@
1
1
  import typing_extensions as typing
2
2
 
3
- from telegrinder.api import API
4
- from telegrinder.bot.dispatch import ABCDispatch, Dispatch
5
- from telegrinder.bot.polling import ABCPolling, Polling
3
+ from telegrinder.api.api import API
4
+ from telegrinder.bot.dispatch.abc import ABCDispatch
5
+ from telegrinder.bot.dispatch.dispatch import Dispatch
6
+ from telegrinder.bot.polling.abc import ABCPolling
7
+ from telegrinder.bot.polling.polling import Polling
6
8
  from telegrinder.modules import logger
7
- from telegrinder.tools.loop_wrapper import ABCLoopWrapper, LoopWrapper
9
+ from telegrinder.tools.loop_wrapper import ABCLoopWrapper
10
+ from telegrinder.tools.loop_wrapper.loop_wrapper import LoopWrapper
8
11
 
9
12
  DispatchT = typing.TypeVar("DispatchT", bound=ABCDispatch, default=Dispatch)
10
13
  PollingT = typing.TypeVar("PollingT", bound=ABCPolling, default=Polling)
@@ -43,7 +46,12 @@ class Telegrinder(typing.Generic[DispatchT, PollingT, LoopWrapperT]):
43
46
  return
44
47
  await self.api.delete_webhook()
45
48
 
46
- async def run_polling(self, *, offset: int = 0, skip_updates: bool = False) -> None:
49
+ async def run_polling(
50
+ self,
51
+ *,
52
+ offset: int = 0,
53
+ skip_updates: bool = False,
54
+ ) -> None:
47
55
  if skip_updates:
48
56
  logger.debug("Dropping pending updates")
49
57
  await self.reset_webhook()
@@ -52,7 +60,11 @@ class Telegrinder(typing.Generic[DispatchT, PollingT, LoopWrapperT]):
52
60
 
53
61
  async for updates in self.polling.listen():
54
62
  for update in updates:
55
- logger.debug("Received update (update_id={})", update.update_id)
63
+ logger.debug(
64
+ "Received update (update_id={}, update_type={!r})",
65
+ update.update_id,
66
+ update.update_type.name,
67
+ )
56
68
  self.loop_wrapper.add_task(self.dispatch.feed(update, self.api))
57
69
 
58
70
  def run_forever(self, *, offset: int = 0, skip_updates: bool = False) -> None:
@@ -1,10 +1,10 @@
1
- from .base import BaseCute
2
- from .callback_query import CallbackQueryCute
3
- from .chat_join_request import ChatJoinRequestCute
4
- from .chat_member_updated import ChatMemberUpdatedCute
5
- from .inline_query import InlineQueryCute
6
- from .message import MessageCute
7
- from .update import UpdateCute
1
+ from telegrinder.bot.cute_types.base import BaseCute
2
+ from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
3
+ from telegrinder.bot.cute_types.chat_join_request import ChatJoinRequestCute
4
+ from telegrinder.bot.cute_types.chat_member_updated import ChatMemberUpdatedCute
5
+ from telegrinder.bot.cute_types.inline_query import InlineQueryCute
6
+ from telegrinder.bot.cute_types.message import MessageCute
7
+ from telegrinder.bot.cute_types.update import UpdateCute
8
8
 
9
9
  __all__ = (
10
10
  "BaseCute",
@@ -3,14 +3,16 @@ import inspect
3
3
  import typing
4
4
  from functools import wraps
5
5
 
6
+ import typing_extensions
6
7
  from fntypes.result import Result
7
8
 
8
- from telegrinder.api import ABCAPI, API
9
+ from telegrinder.api.api import API
9
10
  from telegrinder.model import Model, get_params
10
11
 
11
12
  F = typing.TypeVar("F", bound=typing.Callable[..., typing.Any])
12
13
  Cute = typing.TypeVar("Cute", bound="BaseCute")
13
- Update = typing.TypeVar("Update", bound=Model)
14
+ Update = typing_extensions.TypeVar("Update", bound=Model)
15
+ CtxAPI = typing_extensions.TypeVar("CtxAPI", bound=API, default=API)
14
16
 
15
17
  Executor: typing.TypeAlias = typing.Callable[
16
18
  [Cute, str, dict[str, typing.Any]],
@@ -19,31 +21,124 @@ Executor: typing.TypeAlias = typing.Callable[
19
21
 
20
22
  if typing.TYPE_CHECKING:
21
23
 
22
- class BaseCute(Model, typing.Generic[Update]):
23
- api: ABCAPI
24
+ class BaseCute(Model, typing.Generic[Update, CtxAPI]):
25
+ api: API
24
26
 
25
27
  @classmethod
26
- def from_update(cls, update: Update, bound_api: ABCAPI) -> typing.Self: ...
28
+ def from_update(cls, update: Update, bound_api: API) -> typing.Self: ...
27
29
 
28
30
  @property
29
- def ctx_api(self) -> API: ...
31
+ def ctx_api(self) -> CtxAPI: ...
32
+
33
+ def to_dict(
34
+ self,
35
+ *,
36
+ exclude_fields: set[str] | None = None,
37
+ ) -> dict[str, typing.Any]:
38
+ """
39
+ :param exclude_fields: Cute model field names to exclude from the dictionary representation of this cute model.
40
+ :return: A dictionary representation of this cute model.
41
+ """
42
+
43
+ ...
44
+
45
+ def to_full_dict(
46
+ self,
47
+ *,
48
+ exclude_fields: set[str] | None = None,
49
+ ) -> dict[str, typing.Any]:
50
+ """
51
+ :param exclude_fields: Cute model field names to exclude from the dictionary representation of this cute model.
52
+ :return: A dictionary representation of this model including all models, structs, custom types.
53
+ """
54
+
55
+ ...
30
56
 
31
57
  else:
58
+ from fntypes.co import Some, Variative
59
+
60
+ from telegrinder.msgspec_utils import Option, decoder
61
+ from telegrinder.msgspec_utils import get_class_annotations as _get_class_annotations
62
+
63
+ def _get_cute_from_args(args):
64
+ for hint in args:
65
+ if not isinstance(hint, type):
66
+ hint = typing.get_origin(hint) or hint
67
+ if not isinstance(hint, type):
68
+ continue
69
+
70
+ if hint in (Variative, Some, Option):
71
+ return _get_cute_from_args(typing.get_args(hint))
72
+ if issubclass(hint, BaseCute):
73
+ return hint
74
+
75
+ return None
76
+
77
+ def _get_cute_annotations(annotations):
78
+ cute_annotations = {}
79
+
80
+ for key, hint in annotations.items():
81
+ if not isinstance(hint, type):
82
+ if (val := _get_cute_from_args(typing.get_args(hint))) is not None:
83
+ cute_annotations[key] = val
84
+
85
+ elif issubclass(hint, BaseCute):
86
+ cute_annotations[key] = hint
87
+
88
+ return cute_annotations
89
+
90
+ def _get_value(value):
91
+ while isinstance(value, Variative | Some):
92
+ if isinstance(value, Variative):
93
+ value = value.v
94
+ if isinstance(value, Some):
95
+ value = value.value
96
+ return value
97
+
98
+ class BaseCute(typing.Generic[Update, CtxAPI]):
99
+ def __init_subclass__(cls, *args, **kwargs):
100
+ super().__init_subclass__(*args, **kwargs)
101
+
102
+ if not cls.__bases__ or not issubclass(cls.__bases__[0], BaseCute):
103
+ return
104
+
105
+ cls.__is_solved_annotations__ = False
106
+ cls.__cute_annotations__ = None
32
107
 
33
- class BaseCute(typing.Generic[Update]):
34
108
  @classmethod
35
109
  def from_update(cls, update, bound_api):
36
- return cls(**update.to_dict(), api=bound_api)
110
+ if not cls.__is_solved_annotations__:
111
+ cls.__is_solved_annotations__ = True
112
+ cls.__annotations__ = _get_class_annotations(cls)
113
+
114
+ if cls.__cute_annotations__ is None:
115
+ cls.__cute_annotations__ = _get_cute_annotations(cls.__annotations__)
116
+
117
+ return cls(
118
+ **{
119
+ field: decoder.convert(
120
+ cls.__cute_annotations__[field].from_update(_get_value(value), bound_api=bound_api),
121
+ type=cls.__annotations__[field],
122
+ )
123
+ if field in cls.__cute_annotations__ and value
124
+ else value
125
+ for field, value in update.to_dict().items()
126
+ },
127
+ api=bound_api,
128
+ )
37
129
 
38
130
  @property
39
131
  def ctx_api(self):
40
- assert isinstance(self.api, API)
41
132
  return self.api
42
133
 
43
134
  def to_dict(self, *, exclude_fields=None):
44
135
  exclude_fields = exclude_fields or set()
45
136
  return super().to_dict(exclude_fields={"api"} | exclude_fields)
46
137
 
138
+ def to_full_dict(self, *, exclude_fields=None):
139
+ exclude_fields = exclude_fields or set()
140
+ return super().to_full_dict(exclude_fields={"api"} | exclude_fields)
141
+
47
142
 
48
143
  def compose_method_params(
49
144
  params: dict[str, typing.Any],
@@ -57,12 +152,12 @@ def compose_method_params(
57
152
  :param params: Method params.
58
153
  :param update: Update object.
59
154
  :param default_params: Default params. \
60
- type (`str`) - Attribute name to be taken from `update` if param undefined. \
61
- type (`tuple[str, str]`) - tuple[0] Parameter name to be set in `params`, \
62
- tuple[1] attribute name to be taken from `update`.
155
+ (`str`) - Attribute name to be get from `update` if param is undefined. \
156
+ (`tuple[str, str]`): tuple[0] - Parameter name to be set in `params`, \
157
+ tuple[1] - attribute name to be get from `update`.
63
158
  :param validators: Validators mapping (`str, Callable`), key - `Parameter name` \
64
159
  for which the validator will be applied, value - `Validator`, if returned `True` \
65
- parameter will be set, otherwise will not be set.
160
+ parameter will be set, otherwise will not.
66
161
  :return: Composed params.
67
162
  """
68
163
 
@@ -79,8 +174,6 @@ def compose_method_params(
79
174
  return params
80
175
 
81
176
 
82
- # NOTE: implement parser on ast for methods decorated this decorator
83
- # to support updates to the schema Bot API.
84
177
  def shortcut(
85
178
  method_name: str,
86
179
  *,
@@ -96,7 +189,15 @@ def shortcut(
96
189
  ) -> typing.Any:
97
190
  if executor is None:
98
191
  return await func(self, *args, **kwargs)
99
- signature_params = {k: p for k, p in inspect.signature(func).parameters.items() if k != "self"}
192
+
193
+ if not hasattr(func, "_signature_params"):
194
+ setattr(
195
+ func,
196
+ "_signature_params",
197
+ {k: p for k, p in inspect.signature(func).parameters.items() if k != "self"},
198
+ )
199
+
200
+ signature_params: dict[str, inspect.Parameter] = getattr(func, "_signature_params")
100
201
  params: dict[str, typing.Any] = {}
101
202
  index = 0
102
203
 
@@ -105,9 +206,11 @@ def shortcut(
105
206
  params[k] = args[index]
106
207
  index += 1
107
208
  continue
209
+
108
210
  if p.kind in (p.VAR_KEYWORD, p.VAR_POSITIONAL):
109
211
  params[k] = kwargs.copy() if p.kind is p.VAR_KEYWORD else args[index:]
110
212
  continue
213
+
111
214
  params[k] = kwargs.pop(k, p.default) if p.default is not p.empty else kwargs.pop(k)
112
215
 
113
216
  return await executor(self, method_name, get_params(params))
@@ -122,12 +225,11 @@ def shortcut(
122
225
  return wrapper
123
226
 
124
227
 
125
- @dataclasses.dataclass
228
+ @dataclasses.dataclass(slots=True, frozen=True)
126
229
  class Shortcut:
127
230
  method_name: str
128
- _: dataclasses.KW_ONLY
129
- executor: Executor | None = dataclasses.field(default=None)
130
- custom_params: set[str] = dataclasses.field(default_factory=lambda: set())
231
+ executor: Executor | None = dataclasses.field(default=None, kw_only=True)
232
+ custom_params: set[str] = dataclasses.field(default_factory=lambda: set(), kw_only=True)
131
233
 
132
234
 
133
235
  __all__ = ("BaseCute", "Shortcut", "compose_method_params", "shortcut")
@@ -4,12 +4,15 @@ from contextlib import suppress
4
4
  import msgspec
5
5
  from fntypes.co import Nothing, Result, Some, Variative, unwrapping
6
6
 
7
- from telegrinder.api import ABCAPI, APIError
7
+ from telegrinder.api import API, APIError
8
+ from telegrinder.bot.cute_types.base import BaseCute, compose_method_params, shortcut
9
+ from telegrinder.bot.cute_types.message import MediaType, MessageCute, ReplyMarkup, execute_method_edit
8
10
  from telegrinder.model import get_params
9
11
  from telegrinder.msgspec_utils import Option, decoder
10
- from telegrinder.types import (
12
+ from telegrinder.types.objects import (
11
13
  CallbackQuery,
12
14
  Chat,
15
+ InaccessibleMessage,
13
16
  InlineKeyboardMarkup,
14
17
  InputFile,
15
18
  InputMedia,
@@ -20,12 +23,13 @@ from telegrinder.types import (
20
23
  User,
21
24
  )
22
25
 
23
- from .base import BaseCute, compose_method_params, shortcut
24
- from .message import MediaType, MessageCute, ReplyMarkup, execute_method_edit
25
26
 
27
+ class CallbackQueryCute(BaseCute[CallbackQuery], CallbackQuery, kw_only=True):
28
+ api: API
26
29
 
27
- class CallbackQueryCute(BaseCute[CallbackQuery], CallbackQuery, kw_only=True, dict=True):
28
- api: ABCAPI
30
+ message: Option[Variative[MessageCute, InaccessibleMessage]] = Nothing()
31
+ """Optional. Message sent by the bot with the callback button that originated
32
+ the query."""
29
33
 
30
34
  @property
31
35
  def from_user(self) -> User:
@@ -2,15 +2,14 @@ import typing
2
2
 
3
3
  from fntypes.result import Result
4
4
 
5
- from telegrinder.api.abc import ABCAPI, APIError
5
+ from telegrinder.api import API, APIError
6
+ from telegrinder.bot.cute_types.base import BaseCute, shortcut
7
+ from telegrinder.bot.cute_types.chat_member_updated import ChatMemberShortcuts, chat_member_interaction
6
8
  from telegrinder.types.objects import ChatJoinRequest, User
7
9
 
8
- from .base import BaseCute, shortcut
9
- from .chat_member_updated import ChatMemberShortcuts, chat_member_interaction
10
-
11
10
 
12
11
  class ChatJoinRequestCute(BaseCute[ChatJoinRequest], ChatJoinRequest, ChatMemberShortcuts, kw_only=True):
13
- api: ABCAPI
12
+ api: API
14
13
 
15
14
  @property
16
15
  def from_user(self) -> User:
@@ -3,13 +3,11 @@ from datetime import datetime
3
3
 
4
4
  from fntypes.result import Result
5
5
 
6
- from telegrinder.api import ABCAPI, APIError
6
+ from telegrinder.api import API, APIError
7
+ from telegrinder.bot.cute_types.base import BaseCute, compose_method_params, shortcut
7
8
  from telegrinder.model import get_params
8
9
  from telegrinder.types.objects import ChatMemberUpdated, ChatPermissions, User
9
10
 
10
- from .base import BaseCute, compose_method_params, shortcut
11
- from .utils import compose_chat_permissions
12
-
13
11
 
14
12
  async def chat_member_interaction(
15
13
  update: BaseCute[typing.Any],
@@ -17,7 +15,7 @@ async def chat_member_interaction(
17
15
  params: dict[str, typing.Any],
18
16
  ) -> Result[typing.Any, APIError]:
19
17
  if isinstance(params.get("permissions"), dict):
20
- params["permissions"] = compose_chat_permissions(**params["permissions"])
18
+ params["permissions"] = ChatPermissions(**params["permissions"])
21
19
  params = compose_method_params(
22
20
  get_params(locals()),
23
21
  update,
@@ -235,7 +233,7 @@ class ChatMemberShortcuts:
235
233
  class ChatMemberUpdatedCute(
236
234
  BaseCute[ChatMemberUpdated], ChatMemberUpdated, ChatMemberShortcuts, kw_only=True
237
235
  ):
238
- api: ABCAPI
236
+ api: API
239
237
 
240
238
  @property
241
239
  def from_user(self) -> User:
@@ -2,7 +2,8 @@ import typing
2
2
 
3
3
  from fntypes.result import Result
4
4
 
5
- from telegrinder.api import ABCAPI, APIError
5
+ from telegrinder.api import API, APIError
6
+ from telegrinder.bot.cute_types.base import BaseCute, compose_method_params, shortcut
6
7
  from telegrinder.model import get_params
7
8
  from telegrinder.types import (
8
9
  InlineQuery,
@@ -11,11 +12,9 @@ from telegrinder.types import (
11
12
  User,
12
13
  )
13
14
 
14
- from .base import BaseCute, compose_method_params, shortcut
15
-
16
15
 
17
16
  class InlineQueryCute(BaseCute[InlineQuery], InlineQuery, kw_only=True):
18
- api: ABCAPI
17
+ api: API
19
18
 
20
19
  @property
21
20
  def from_user(self) -> User: