telegrinder 0.4.1__py3-none-any.whl → 0.5.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 (233) hide show
  1. telegrinder/__init__.py +38 -56
  2. telegrinder/__meta__.py +1 -0
  3. telegrinder/api/__init__.py +6 -4
  4. telegrinder/api/api.py +100 -26
  5. telegrinder/api/error.py +42 -8
  6. telegrinder/api/response.py +4 -1
  7. telegrinder/api/token.py +2 -2
  8. telegrinder/bot/__init__.py +9 -25
  9. telegrinder/bot/bot.py +31 -25
  10. telegrinder/bot/cute_types/__init__.py +0 -0
  11. telegrinder/bot/cute_types/base.py +103 -61
  12. telegrinder/bot/cute_types/callback_query.py +447 -400
  13. telegrinder/bot/cute_types/chat_join_request.py +59 -62
  14. telegrinder/bot/cute_types/chat_member_updated.py +154 -157
  15. telegrinder/bot/cute_types/inline_query.py +41 -44
  16. telegrinder/bot/cute_types/message.py +2621 -2590
  17. telegrinder/bot/cute_types/pre_checkout_query.py +38 -42
  18. telegrinder/bot/cute_types/update.py +1 -8
  19. telegrinder/bot/cute_types/utils.py +1 -1
  20. telegrinder/bot/dispatch/__init__.py +10 -15
  21. telegrinder/bot/dispatch/abc.py +12 -11
  22. telegrinder/bot/dispatch/action.py +104 -0
  23. telegrinder/bot/dispatch/context.py +32 -26
  24. telegrinder/bot/dispatch/dispatch.py +61 -134
  25. telegrinder/bot/dispatch/handler/__init__.py +2 -0
  26. telegrinder/bot/dispatch/handler/abc.py +10 -8
  27. telegrinder/bot/dispatch/handler/audio_reply.py +2 -3
  28. telegrinder/bot/dispatch/handler/base.py +10 -33
  29. telegrinder/bot/dispatch/handler/document_reply.py +2 -3
  30. telegrinder/bot/dispatch/handler/func.py +55 -87
  31. telegrinder/bot/dispatch/handler/media_group_reply.py +2 -3
  32. telegrinder/bot/dispatch/handler/message_reply.py +2 -3
  33. telegrinder/bot/dispatch/handler/photo_reply.py +2 -3
  34. telegrinder/bot/dispatch/handler/sticker_reply.py +2 -3
  35. telegrinder/bot/dispatch/handler/video_reply.py +2 -3
  36. telegrinder/bot/dispatch/middleware/__init__.py +0 -0
  37. telegrinder/bot/dispatch/middleware/abc.py +79 -55
  38. telegrinder/bot/dispatch/middleware/global_middleware.py +18 -33
  39. telegrinder/bot/dispatch/process.py +84 -105
  40. telegrinder/bot/dispatch/return_manager/__init__.py +0 -0
  41. telegrinder/bot/dispatch/return_manager/abc.py +102 -65
  42. telegrinder/bot/dispatch/return_manager/callback_query.py +4 -5
  43. telegrinder/bot/dispatch/return_manager/inline_query.py +3 -4
  44. telegrinder/bot/dispatch/return_manager/message.py +8 -10
  45. telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +4 -5
  46. telegrinder/bot/dispatch/view/__init__.py +4 -4
  47. telegrinder/bot/dispatch/view/abc.py +6 -16
  48. telegrinder/bot/dispatch/view/base.py +54 -178
  49. telegrinder/bot/dispatch/view/box.py +19 -18
  50. telegrinder/bot/dispatch/view/callback_query.py +4 -8
  51. telegrinder/bot/dispatch/view/chat_join_request.py +5 -6
  52. telegrinder/bot/dispatch/view/chat_member.py +5 -25
  53. telegrinder/bot/dispatch/view/error.py +9 -0
  54. telegrinder/bot/dispatch/view/inline_query.py +4 -8
  55. telegrinder/bot/dispatch/view/message.py +5 -25
  56. telegrinder/bot/dispatch/view/pre_checkout_query.py +4 -8
  57. telegrinder/bot/dispatch/view/raw.py +3 -109
  58. telegrinder/bot/dispatch/waiter_machine/__init__.py +2 -5
  59. telegrinder/bot/dispatch/waiter_machine/actions.py +6 -4
  60. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +1 -3
  61. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +1 -1
  62. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +11 -7
  63. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +0 -0
  64. telegrinder/bot/dispatch/waiter_machine/machine.py +43 -60
  65. telegrinder/bot/dispatch/waiter_machine/middleware.py +19 -23
  66. telegrinder/bot/dispatch/waiter_machine/short_state.py +6 -5
  67. telegrinder/bot/polling/__init__.py +0 -0
  68. telegrinder/bot/polling/abc.py +0 -0
  69. telegrinder/bot/polling/polling.py +209 -88
  70. telegrinder/bot/rules/__init__.py +3 -16
  71. telegrinder/bot/rules/abc.py +42 -122
  72. telegrinder/bot/rules/callback_data.py +29 -49
  73. telegrinder/bot/rules/chat_join.py +5 -23
  74. telegrinder/bot/rules/command.py +8 -4
  75. telegrinder/bot/rules/enum_text.py +3 -4
  76. telegrinder/bot/rules/func.py +7 -14
  77. telegrinder/bot/rules/fuzzy.py +3 -4
  78. telegrinder/bot/rules/inline.py +8 -20
  79. telegrinder/bot/rules/integer.py +2 -3
  80. telegrinder/bot/rules/is_from.py +12 -11
  81. telegrinder/bot/rules/logic.py +11 -5
  82. telegrinder/bot/rules/markup.py +22 -14
  83. telegrinder/bot/rules/mention.py +8 -7
  84. telegrinder/bot/rules/message_entities.py +8 -4
  85. telegrinder/bot/rules/node.py +23 -12
  86. telegrinder/bot/rules/payload.py +5 -4
  87. telegrinder/bot/rules/payment_invoice.py +6 -21
  88. telegrinder/bot/rules/regex.py +2 -4
  89. telegrinder/bot/rules/rule_enum.py +8 -7
  90. telegrinder/bot/rules/start.py +5 -6
  91. telegrinder/bot/rules/state.py +1 -1
  92. telegrinder/bot/rules/text.py +4 -15
  93. telegrinder/bot/rules/update.py +3 -4
  94. telegrinder/bot/scenario/__init__.py +0 -0
  95. telegrinder/bot/scenario/abc.py +6 -5
  96. telegrinder/bot/scenario/checkbox.py +1 -1
  97. telegrinder/bot/scenario/choice.py +30 -39
  98. telegrinder/client/__init__.py +3 -5
  99. telegrinder/client/abc.py +11 -6
  100. telegrinder/client/aiohttp.py +141 -27
  101. telegrinder/client/form_data.py +1 -1
  102. telegrinder/model.py +61 -89
  103. telegrinder/modules.py +325 -102
  104. telegrinder/msgspec_utils/__init__.py +40 -0
  105. telegrinder/msgspec_utils/abc.py +18 -0
  106. telegrinder/msgspec_utils/custom_types/__init__.py +6 -0
  107. telegrinder/msgspec_utils/custom_types/datetime.py +24 -0
  108. telegrinder/msgspec_utils/custom_types/enum_meta.py +43 -0
  109. telegrinder/msgspec_utils/custom_types/literal.py +25 -0
  110. telegrinder/msgspec_utils/custom_types/option.py +17 -0
  111. telegrinder/msgspec_utils/decoder.py +389 -0
  112. telegrinder/msgspec_utils/encoder.py +206 -0
  113. telegrinder/{msgspec_json.py → msgspec_utils/json.py} +6 -5
  114. telegrinder/msgspec_utils/tools.py +75 -0
  115. telegrinder/node/__init__.py +24 -7
  116. telegrinder/node/attachment.py +1 -0
  117. telegrinder/node/base.py +154 -72
  118. telegrinder/node/callback_query.py +5 -5
  119. telegrinder/node/collection.py +39 -0
  120. telegrinder/node/command.py +1 -2
  121. telegrinder/node/composer.py +121 -72
  122. telegrinder/node/container.py +11 -8
  123. telegrinder/node/context.py +48 -0
  124. telegrinder/node/either.py +27 -40
  125. telegrinder/node/error.py +41 -0
  126. telegrinder/node/event.py +37 -11
  127. telegrinder/node/exceptions.py +7 -0
  128. telegrinder/node/file.py +0 -0
  129. telegrinder/node/i18n.py +108 -0
  130. telegrinder/node/me.py +3 -2
  131. telegrinder/node/payload.py +1 -1
  132. telegrinder/node/polymorphic.py +63 -28
  133. telegrinder/node/reply_message.py +12 -0
  134. telegrinder/node/rule.py +6 -13
  135. telegrinder/node/scope.py +14 -5
  136. telegrinder/node/session.py +53 -0
  137. telegrinder/node/source.py +41 -9
  138. telegrinder/node/text.py +1 -2
  139. telegrinder/node/tools/__init__.py +0 -0
  140. telegrinder/node/tools/generator.py +3 -5
  141. telegrinder/node/utility.py +16 -0
  142. telegrinder/py.typed +0 -0
  143. telegrinder/rules.py +0 -0
  144. telegrinder/tools/__init__.py +48 -88
  145. telegrinder/tools/aio.py +103 -0
  146. telegrinder/tools/callback_data_serialization/__init__.py +5 -0
  147. telegrinder/tools/{callback_data_serilization → callback_data_serialization}/abc.py +0 -0
  148. telegrinder/tools/{callback_data_serilization → callback_data_serialization}/json_ser.py +2 -3
  149. telegrinder/tools/{callback_data_serilization → callback_data_serialization}/msgpack_ser.py +45 -27
  150. telegrinder/tools/final.py +21 -0
  151. telegrinder/tools/formatting/__init__.py +2 -18
  152. telegrinder/tools/formatting/deep_links/__init__.py +39 -0
  153. telegrinder/tools/formatting/{deep_links.py → deep_links/links.py} +12 -85
  154. telegrinder/tools/formatting/deep_links/parsing.py +90 -0
  155. telegrinder/tools/formatting/deep_links/validators.py +8 -0
  156. telegrinder/tools/formatting/html_formatter.py +18 -45
  157. telegrinder/tools/fullname.py +83 -0
  158. telegrinder/tools/global_context/__init__.py +4 -3
  159. telegrinder/tools/global_context/abc.py +17 -14
  160. telegrinder/tools/global_context/builtin_context.py +39 -0
  161. telegrinder/tools/global_context/global_context.py +138 -39
  162. telegrinder/tools/input_file_directory.py +0 -0
  163. telegrinder/tools/keyboard/__init__.py +39 -0
  164. telegrinder/tools/keyboard/abc.py +159 -0
  165. telegrinder/tools/keyboard/base.py +77 -0
  166. telegrinder/tools/keyboard/buttons/__init__.py +14 -0
  167. telegrinder/tools/keyboard/buttons/base.py +18 -0
  168. telegrinder/tools/{buttons.py → keyboard/buttons/buttons.py} +71 -23
  169. telegrinder/tools/keyboard/buttons/static_buttons.py +56 -0
  170. telegrinder/tools/keyboard/buttons/tools.py +18 -0
  171. telegrinder/tools/keyboard/data.py +20 -0
  172. telegrinder/tools/keyboard/keyboard.py +131 -0
  173. telegrinder/tools/keyboard/static_keyboard.py +83 -0
  174. telegrinder/tools/lifespan.py +87 -51
  175. telegrinder/tools/limited_dict.py +4 -1
  176. telegrinder/tools/loop_wrapper.py +332 -0
  177. telegrinder/tools/magic/__init__.py +32 -0
  178. telegrinder/tools/magic/annotations.py +165 -0
  179. telegrinder/tools/magic/dictionary.py +20 -0
  180. telegrinder/tools/magic/function.py +246 -0
  181. telegrinder/tools/magic/shortcut.py +111 -0
  182. telegrinder/tools/parse_mode.py +9 -3
  183. telegrinder/tools/singleton/__init__.py +4 -0
  184. telegrinder/tools/singleton/abc.py +14 -0
  185. telegrinder/tools/singleton/singleton.py +18 -0
  186. telegrinder/tools/state_storage/__init__.py +0 -0
  187. telegrinder/tools/state_storage/abc.py +6 -1
  188. telegrinder/tools/state_storage/memory.py +1 -1
  189. telegrinder/tools/strings.py +0 -0
  190. telegrinder/types/__init__.py +307 -268
  191. telegrinder/types/enums.py +64 -37
  192. telegrinder/types/input_file.py +3 -3
  193. telegrinder/types/methods.py +5699 -5055
  194. telegrinder/types/methods_utils.py +62 -0
  195. telegrinder/types/objects.py +7846 -7058
  196. telegrinder/verification_utils.py +3 -1
  197. telegrinder-0.5.0.dist-info/METADATA +162 -0
  198. telegrinder-0.5.0.dist-info/RECORD +200 -0
  199. {telegrinder-0.4.1.dist-info → telegrinder-0.5.0.dist-info}/WHEEL +1 -1
  200. {telegrinder-0.4.1.dist-info → telegrinder-0.5.0.dist-info/licenses}/LICENSE +2 -2
  201. telegrinder/bot/dispatch/waiter_machine/hasher/state.py +0 -20
  202. telegrinder/bot/rules/id.py +0 -24
  203. telegrinder/bot/rules/message.py +0 -15
  204. telegrinder/client/sonic.py +0 -212
  205. telegrinder/msgspec_utils.py +0 -478
  206. telegrinder/tools/adapter/__init__.py +0 -19
  207. telegrinder/tools/adapter/abc.py +0 -49
  208. telegrinder/tools/adapter/dataclass.py +0 -56
  209. telegrinder/tools/adapter/errors.py +0 -5
  210. telegrinder/tools/adapter/event.py +0 -63
  211. telegrinder/tools/adapter/node.py +0 -46
  212. telegrinder/tools/adapter/raw_event.py +0 -27
  213. telegrinder/tools/adapter/raw_update.py +0 -30
  214. telegrinder/tools/callback_data_serilization/__init__.py +0 -5
  215. telegrinder/tools/error_handler/__init__.py +0 -10
  216. telegrinder/tools/error_handler/abc.py +0 -30
  217. telegrinder/tools/error_handler/error.py +0 -9
  218. telegrinder/tools/error_handler/error_handler.py +0 -179
  219. telegrinder/tools/formatting/spec_html_formats.py +0 -75
  220. telegrinder/tools/functional.py +0 -8
  221. telegrinder/tools/global_context/telegrinder_ctx.py +0 -27
  222. telegrinder/tools/i18n/__init__.py +0 -12
  223. telegrinder/tools/i18n/abc.py +0 -32
  224. telegrinder/tools/i18n/middleware/__init__.py +0 -3
  225. telegrinder/tools/i18n/middleware/abc.py +0 -22
  226. telegrinder/tools/i18n/simple.py +0 -43
  227. telegrinder/tools/keyboard.py +0 -132
  228. telegrinder/tools/loop_wrapper/__init__.py +0 -4
  229. telegrinder/tools/loop_wrapper/abc.py +0 -20
  230. telegrinder/tools/loop_wrapper/loop_wrapper.py +0 -169
  231. telegrinder/tools/magic.py +0 -344
  232. telegrinder-0.4.1.dist-info/METADATA +0 -143
  233. telegrinder-0.4.1.dist-info/RECORD +0 -182
telegrinder/bot/bot.py CHANGED
@@ -1,36 +1,42 @@
1
+ from __future__ import annotations
2
+
1
3
  import typing_extensions as typing
2
4
 
3
- from telegrinder.api.api import API, HTTPClient
5
+ from telegrinder.api.api import API
4
6
  from telegrinder.bot.dispatch import dispatch as dp
5
7
  from telegrinder.bot.dispatch.abc import ABCDispatch
6
8
  from telegrinder.bot.polling import polling as pg
7
9
  from telegrinder.bot.polling.abc import ABCPolling
8
10
  from telegrinder.modules import logger
9
- from telegrinder.tools.loop_wrapper import ABCLoopWrapper
10
- from telegrinder.tools.loop_wrapper import loop_wrapper as lw
11
+ from telegrinder.tools.global_context.builtin_context import TelegrinderContext
12
+ from telegrinder.tools.loop_wrapper import LoopWrapper
13
+
14
+ if typing.TYPE_CHECKING:
15
+ from telegrinder.node.composer import Composer
11
16
 
12
- Dispatch = typing.TypeVar("Dispatch", bound=ABCDispatch, default=dp.Dispatch[HTTPClient])
13
- Polling = typing.TypeVar("Polling", bound=ABCPolling, default=pg.Polling[HTTPClient])
14
- LoopWrapper = typing.TypeVar("LoopWrapper", bound=ABCLoopWrapper, default=lw.LoopWrapper)
17
+ Dispatch = typing.TypeVar("Dispatch", bound=ABCDispatch, default=dp.Dispatch)
18
+ Polling = typing.TypeVar("Polling", bound=ABCPolling, default=pg.Polling)
15
19
 
20
+ CONTEXT: typing.Final[TelegrinderContext] = TelegrinderContext()
16
21
 
17
- class Telegrinder(typing.Generic[HTTPClient, Dispatch, Polling, LoopWrapper]):
22
+
23
+ class Telegrinder(typing.Generic[Dispatch, Polling]):
18
24
  def __init__(
19
25
  self,
20
- api: API[HTTPClient],
26
+ api: API,
21
27
  *,
22
28
  dispatch: Dispatch | None = None,
23
29
  polling: Polling | None = None,
24
30
  loop_wrapper: LoopWrapper | None = None,
25
31
  ) -> None:
26
32
  self.api = api
27
- self.dispatch = typing.cast(Dispatch, dispatch or dp.Dispatch())
28
- self.polling = typing.cast(Polling, polling or pg.Polling(api))
29
- self.loop_wrapper = typing.cast(LoopWrapper, loop_wrapper or lw.LoopWrapper())
33
+ self.dispatch = typing.cast("Dispatch", dispatch or dp.Dispatch())
34
+ self.polling = typing.cast("Polling", polling or pg.Polling(api))
35
+ self.loop_wrapper = loop_wrapper or CONTEXT.loop_wrapper
30
36
 
31
37
  def __repr__(self) -> str:
32
38
  return "<{}: api={!r}, dispatch={!r}, polling={!r}, loop_wrapper={!r}>".format(
33
- self.__class__.__name__,
39
+ type(self).__name__,
34
40
  self.api,
35
41
  self.dispatch,
36
42
  self.polling,
@@ -41,6 +47,10 @@ class Telegrinder(typing.Generic[HTTPClient, Dispatch, Polling, LoopWrapper]):
41
47
  def on(self) -> Dispatch:
42
48
  return self.dispatch
43
49
 
50
+ @property
51
+ def composer(self) -> Composer:
52
+ return CONTEXT.composer.unwrap()
53
+
44
54
  async def reset_webhook(self) -> None:
45
55
  if not (await self.api.get_webhook_info()).unwrap().url:
46
56
  return
@@ -52,32 +62,28 @@ class Telegrinder(typing.Generic[HTTPClient, Dispatch, Polling, LoopWrapper]):
52
62
  offset: int = 0,
53
63
  skip_updates: bool = False,
54
64
  ) -> typing.NoReturn:
55
- async def polling() -> typing.NoReturn:
65
+ async def polling() -> typing.NoReturn: # type: ignore
56
66
  if skip_updates:
57
67
  logger.debug("Dropping pending updates")
58
68
  await self.reset_webhook()
59
69
  await self.api.delete_webhook(drop_pending_updates=True)
60
- self.polling.offset = offset
61
70
 
62
71
  async for updates in self.polling.listen():
63
72
  for update in updates:
64
- logger.debug(
65
- "Received update (update_id={}, update_type={!r})",
66
- update.update_id,
67
- update.update_type.name,
68
- )
69
- self.loop_wrapper.add_task(self.dispatch.feed(update, self.api))
70
-
71
- if self.loop_wrapper.is_running:
73
+ await self.loop_wrapper.create_task(self.dispatch.feed(update, self.api))
74
+
75
+ self.polling.offset = offset
76
+
77
+ if self.loop_wrapper.running:
72
78
  await polling()
73
79
  else:
74
80
  self.loop_wrapper.add_task(polling())
75
- self.loop_wrapper.run_event_loop()
81
+ self.loop_wrapper.run()
76
82
 
77
83
  def run_forever(self, *, offset: int = 0, skip_updates: bool = False) -> typing.NoReturn:
78
- logger.debug("Running blocking polling (id={})", self.api.id)
84
+ logger.info("Running blocking polling (id={})", self.api.id)
79
85
  self.loop_wrapper.add_task(self.run_polling(offset=offset, skip_updates=skip_updates))
80
- self.loop_wrapper.run_event_loop()
86
+ self.loop_wrapper.run()
81
87
 
82
88
 
83
89
  __all__ = ("Telegrinder",)
File without changes
@@ -1,9 +1,7 @@
1
1
  import typing
2
2
 
3
- import msgspec
4
-
5
- from telegrinder.api.api import API
6
- from telegrinder.model import Model, is_none
3
+ from telegrinder.model import Model
4
+ from telegrinder.tools.magic.shortcut import shortcut
7
5
 
8
6
 
9
7
  def compose_method_params[Cute: BaseCute](
@@ -27,46 +25,53 @@ def compose_method_params[Cute: BaseCute](
27
25
 
28
26
 
29
27
  if typing.TYPE_CHECKING:
28
+ from fntypes.option import Option
29
+
30
+ from telegrinder.api.api import API
30
31
  from telegrinder.node.base import Node
32
+ from telegrinder.types.objects import Update
31
33
 
32
- class BaseCute[Update: Model](Model):
34
+ class BaseCute[T: Model](Model):
33
35
  api: API
34
36
 
35
37
  @classmethod
36
38
  def as_node(cls) -> type[Node]: ...
37
39
 
38
40
  @classmethod
39
- def from_update(cls, update: Update, bound_api: API) -> typing.Self: ...
41
+ def from_update(cls, update: T, bound_api: API) -> typing.Self: ...
40
42
 
41
43
  @property
42
44
  def ctx_api(self) -> API: ...
43
45
 
46
+ @property
47
+ def raw_update(self) -> Update: ...
48
+
49
+ def bind_raw_update(self, raw_update: Update, /) -> typing.Self: ...
50
+
51
+ def get_raw_update(self) -> Option[Update]: ...
52
+
44
53
  def to_dict(
45
54
  self,
46
55
  *,
47
56
  exclude_fields: set[str] | None = None,
48
- ) -> dict[str, typing.Any]:
49
- """:param exclude_fields: Cute model field names to exclude from the dictionary representation of this cute model.
50
- :return: A dictionary representation of this cute model.
51
- """
52
- ...
57
+ ) -> dict[str, typing.Any]: ...
53
58
 
54
59
  def to_full_dict(
55
60
  self,
56
61
  *,
57
62
  exclude_fields: set[str] | None = None,
58
- ) -> dict[str, typing.Any]:
59
- """:param exclude_fields: Cute model field names to exclude from the dictionary representation of this cute model.
60
- :return: A dictionary representation of this model including all models, structs, custom types.
61
- """
62
- ...
63
+ ) -> dict[str, typing.Any]: ...
63
64
 
64
65
  else:
65
- import msgspec
66
- from fntypes.co import Nothing, Some, Variative
66
+ from fntypes.co import Some, Variative
67
+ from fntypes.misc import from_optional
67
68
 
68
- from telegrinder.msgspec_utils import Option, decoder, encoder, struct_as_dict
69
+ from telegrinder.msgspec_utils import Option, encoder, struct_asdict
69
70
  from telegrinder.msgspec_utils import get_class_annotations as _get_class_annotations
71
+ from telegrinder.types.objects import Update
72
+
73
+ BOUND_API_KEY = "bound_api"
74
+ RAW_UPDATE_BIND_KEY = "raw_update_bind"
70
75
 
71
76
  def _get_cute_from_generic(generic_args, /):
72
77
  for arg in generic_args:
@@ -94,79 +99,113 @@ else:
94
99
 
95
100
  return cute_annotations
96
101
 
97
- def _validate_value(value, /):
102
+ def _maybe_wrapped(value, /):
103
+ wrapped_value = None
104
+
98
105
  while isinstance(value, Variative | Some):
106
+ wrapped_value = value
107
+
99
108
  if isinstance(value, Variative):
100
109
  value = value.v
110
+
101
111
  if isinstance(value, Some):
102
112
  value = value.value
103
- return value
104
-
105
- class BaseCute[Update]:
106
- api: API
107
113
 
114
+ return wrapped_value if wrapped_value is not None else value
115
+
116
+ def _wrap_value(value, type_):
117
+ args = [type_]
118
+ types = []
119
+
120
+ while args:
121
+ arg = args.pop(0)
122
+ origin_arg = typing.get_origin(arg) or arg
123
+ if issubclass(origin_arg, Variative | Option):
124
+ args.extend(typing.get_args(arg))
125
+ types.append(Some if issubclass(origin_arg, Option) else arg)
126
+
127
+ result = value
128
+ for t in types[::-1]:
129
+ result = t(result)
130
+ return result
131
+
132
+ def _to_cute(cls, field, value, bound_api):
133
+ maybe_wrapped_value = _maybe_wrapped(value)
134
+ is_wrapped_value = isinstance(maybe_wrapped_value, Variative | Some)
135
+ cute = cls.__cute_annotations__[field].from_update(
136
+ maybe_wrapped_value._value if is_wrapped_value else maybe_wrapped_value,
137
+ bound_api=bound_api,
138
+ )
139
+ return _wrap_value(cute, cls.__annotations__[field]) if is_wrapped_value else cute
140
+
141
+ class BaseCute[T]:
108
142
  def __init_subclass__(cls, *args, **kwargs):
109
- super().__init_subclass__(*args, **kwargs)
110
-
111
- if not cls.__bases__ or not issubclass(cls.__bases__[0], BaseCute):
112
- return
113
-
114
- cls.__is_solved_annotations__ = False
143
+ cls.__is_resolved_annotations__ = False
115
144
  cls.__cute_annotations__ = None
116
145
  cls.__event_node__ = None
117
146
 
118
147
  @classmethod
119
148
  def as_node(cls):
120
149
  if cls.__event_node__ is None:
121
- from telegrinder.node.event import _EventNode
150
+ from telegrinder.node.event import EventNode
151
+
152
+ cls.__event_node__ = EventNode[cls]
122
153
 
123
- cls.__event_node__ = _EventNode[cls]
124
154
  return cls.__event_node__
125
155
 
126
156
  @classmethod
127
157
  def from_update(cls, update, bound_api):
128
- if not cls.__is_solved_annotations__:
129
- cls.__is_solved_annotations__ = True
158
+ if not cls.__is_resolved_annotations__:
159
+ cls.__is_resolved_annotations__ = True
130
160
  cls.__annotations__ = _get_class_annotations(cls)
131
161
 
132
162
  if cls.__cute_annotations__ is None:
133
163
  cls.__cute_annotations__ = _get_cute_annotations(cls.__annotations__)
134
164
 
135
- return cls(
165
+ cute = cls(
136
166
  **{
137
- field: decoder.convert(
138
- cls.__cute_annotations__[field].from_update(_validate_value(value), bound_api=bound_api),
139
- type=cls.__annotations__[field],
140
- )
141
- if field in cls.__cute_annotations__ and not isinstance(value, Nothing | msgspec.UnsetType)
142
- else value
167
+ field: _to_cute(cls, field, value, bound_api) if field in cls.__cute_annotations__ else value
143
168
  for field, value in update.to_dict().items()
144
169
  },
145
- api=bound_api,
170
+ )._set_bound_api(api=bound_api)
171
+ return cute.bind_raw_update(update) if isinstance(update, Update) else cute
172
+
173
+ @property
174
+ def api(self):
175
+ return self.__dict__[BOUND_API_KEY]
176
+
177
+ @property
178
+ def raw_update(self):
179
+ return self.get_raw_update().expect(
180
+ ValueError(f"Cute model `{type(self).__name__}` has no bind `Update` object."),
146
181
  )
147
182
 
148
183
  @property
149
184
  def ctx_api(self):
150
185
  return self.api
151
186
 
187
+ def get_raw_update(self):
188
+ return from_optional(self.__dict__.get(RAW_UPDATE_BIND_KEY))
189
+
190
+ def bind_raw_update(self, raw_update, /):
191
+ cuties = [self]
192
+
193
+ if isinstance(self, Update) and isinstance(cute := self.incoming_update, BaseCute):
194
+ cuties.append(cute)
195
+
196
+ for cute in cuties:
197
+ cute.__dict__[RAW_UPDATE_BIND_KEY] = raw_update
198
+
199
+ return self
200
+
201
+ def _set_bound_api(self, api):
202
+ self.__dict__[BOUND_API_KEY] = api
203
+ return self
204
+
152
205
  def _to_dict(self, dct_name, exclude_fields, full):
153
206
  if dct_name not in self.__dict__:
154
- self.__dict__[dct_name] = (
155
- struct_as_dict(self)
156
- if not full
157
- else encoder.to_builtins(
158
- {
159
- k: field.to_dict(exclude_fields=exclude_fields)
160
- if isinstance(field, BaseCute)
161
- else field
162
- for k in self.__struct_fields__
163
- if k not in exclude_fields
164
- and not isinstance(field := _validate_value(getattr(self, k)), msgspec.UnsetType)
165
- and not is_none(field)
166
- },
167
- order="deterministic",
168
- )
169
- )
207
+ dct = struct_asdict(self)
208
+ self.__dict__[dct_name] = dct if not full else encoder.to_builtins(dct, order="deterministic")
170
209
 
171
210
  if not exclude_fields:
172
211
  return self.__dict__[dct_name]
@@ -174,11 +213,14 @@ else:
174
213
  return {key: value for key, value in self.__dict__[dct_name].items() if key not in exclude_fields}
175
214
 
176
215
  def to_dict(self, *, exclude_fields=None, full=False):
177
- exclude_fields = exclude_fields or set()
178
- return self._to_dict("model_as_dict", exclude_fields={"api"} | exclude_fields, full=full)
216
+ return self._to_dict(
217
+ "model_as_dict" if not full else "model_as_full_dict",
218
+ exclude_fields=exclude_fields or set(),
219
+ full=full,
220
+ )
179
221
 
180
222
  def to_full_dict(self, *, exclude_fields=None):
181
223
  return self.to_dict(exclude_fields=exclude_fields, full=True)
182
224
 
183
225
 
184
- __all__ = ("BaseCute", "compose_method_params")
226
+ __all__ = ("BaseCute", "compose_method_params", "shortcut")