telegrinder 0.3.4.post1__py3-none-any.whl → 0.4.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 (169) hide show
  1. telegrinder/__init__.py +30 -31
  2. telegrinder/api/__init__.py +2 -1
  3. telegrinder/api/api.py +28 -20
  4. telegrinder/api/error.py +8 -4
  5. telegrinder/api/response.py +2 -2
  6. telegrinder/api/token.py +2 -2
  7. telegrinder/bot/__init__.py +6 -0
  8. telegrinder/bot/bot.py +38 -31
  9. telegrinder/bot/cute_types/__init__.py +2 -0
  10. telegrinder/bot/cute_types/base.py +54 -128
  11. telegrinder/bot/cute_types/callback_query.py +76 -61
  12. telegrinder/bot/cute_types/chat_join_request.py +4 -3
  13. telegrinder/bot/cute_types/chat_member_updated.py +28 -31
  14. telegrinder/bot/cute_types/inline_query.py +5 -4
  15. telegrinder/bot/cute_types/message.py +555 -602
  16. telegrinder/bot/cute_types/pre_checkout_query.py +42 -0
  17. telegrinder/bot/cute_types/update.py +20 -12
  18. telegrinder/bot/cute_types/utils.py +3 -36
  19. telegrinder/bot/dispatch/__init__.py +4 -0
  20. telegrinder/bot/dispatch/abc.py +8 -9
  21. telegrinder/bot/dispatch/context.py +5 -7
  22. telegrinder/bot/dispatch/dispatch.py +85 -33
  23. telegrinder/bot/dispatch/handler/abc.py +5 -6
  24. telegrinder/bot/dispatch/handler/audio_reply.py +2 -2
  25. telegrinder/bot/dispatch/handler/base.py +3 -3
  26. telegrinder/bot/dispatch/handler/document_reply.py +2 -2
  27. telegrinder/bot/dispatch/handler/func.py +36 -42
  28. telegrinder/bot/dispatch/handler/media_group_reply.py +5 -4
  29. telegrinder/bot/dispatch/handler/message_reply.py +2 -2
  30. telegrinder/bot/dispatch/handler/photo_reply.py +2 -2
  31. telegrinder/bot/dispatch/handler/sticker_reply.py +2 -2
  32. telegrinder/bot/dispatch/handler/video_reply.py +2 -2
  33. telegrinder/bot/dispatch/middleware/abc.py +83 -8
  34. telegrinder/bot/dispatch/middleware/global_middleware.py +70 -0
  35. telegrinder/bot/dispatch/process.py +44 -50
  36. telegrinder/bot/dispatch/return_manager/__init__.py +2 -0
  37. telegrinder/bot/dispatch/return_manager/abc.py +6 -10
  38. telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +20 -0
  39. telegrinder/bot/dispatch/view/__init__.py +2 -0
  40. telegrinder/bot/dispatch/view/abc.py +10 -6
  41. telegrinder/bot/dispatch/view/base.py +81 -50
  42. telegrinder/bot/dispatch/view/box.py +20 -9
  43. telegrinder/bot/dispatch/view/callback_query.py +3 -4
  44. telegrinder/bot/dispatch/view/chat_join_request.py +2 -7
  45. telegrinder/bot/dispatch/view/chat_member.py +3 -5
  46. telegrinder/bot/dispatch/view/inline_query.py +3 -4
  47. telegrinder/bot/dispatch/view/message.py +3 -4
  48. telegrinder/bot/dispatch/view/pre_checkout_query.py +16 -0
  49. telegrinder/bot/dispatch/view/raw.py +42 -40
  50. telegrinder/bot/dispatch/waiter_machine/actions.py +5 -4
  51. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +0 -0
  52. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +0 -0
  53. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +9 -7
  54. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +0 -0
  55. telegrinder/bot/dispatch/waiter_machine/hasher/state.py +3 -2
  56. telegrinder/bot/dispatch/waiter_machine/machine.py +113 -34
  57. telegrinder/bot/dispatch/waiter_machine/middleware.py +15 -10
  58. telegrinder/bot/dispatch/waiter_machine/short_state.py +7 -18
  59. telegrinder/bot/polling/polling.py +62 -54
  60. telegrinder/bot/rules/__init__.py +24 -1
  61. telegrinder/bot/rules/abc.py +17 -10
  62. telegrinder/bot/rules/callback_data.py +20 -61
  63. telegrinder/bot/rules/chat_join.py +6 -4
  64. telegrinder/bot/rules/command.py +4 -4
  65. telegrinder/bot/rules/enum_text.py +1 -4
  66. telegrinder/bot/rules/func.py +5 -3
  67. telegrinder/bot/rules/fuzzy.py +1 -1
  68. telegrinder/bot/rules/id.py +24 -0
  69. telegrinder/bot/rules/inline.py +6 -4
  70. telegrinder/bot/rules/integer.py +2 -1
  71. telegrinder/bot/rules/logic.py +18 -0
  72. telegrinder/bot/rules/markup.py +5 -6
  73. telegrinder/bot/rules/message.py +2 -4
  74. telegrinder/bot/rules/message_entities.py +1 -3
  75. telegrinder/bot/rules/node.py +15 -9
  76. telegrinder/bot/rules/payload.py +81 -0
  77. telegrinder/bot/rules/payment_invoice.py +29 -0
  78. telegrinder/bot/rules/regex.py +5 -6
  79. telegrinder/bot/rules/state.py +1 -3
  80. telegrinder/bot/rules/text.py +10 -5
  81. telegrinder/bot/rules/update.py +0 -0
  82. telegrinder/bot/scenario/abc.py +2 -4
  83. telegrinder/bot/scenario/checkbox.py +12 -14
  84. telegrinder/bot/scenario/choice.py +6 -9
  85. telegrinder/client/__init__.py +9 -1
  86. telegrinder/client/abc.py +35 -10
  87. telegrinder/client/aiohttp.py +28 -24
  88. telegrinder/client/form_data.py +31 -0
  89. telegrinder/client/sonic.py +212 -0
  90. telegrinder/model.py +38 -145
  91. telegrinder/modules.py +3 -1
  92. telegrinder/msgspec_utils.py +136 -68
  93. telegrinder/node/__init__.py +74 -13
  94. telegrinder/node/attachment.py +92 -16
  95. telegrinder/node/base.py +196 -68
  96. telegrinder/node/callback_query.py +17 -16
  97. telegrinder/node/command.py +3 -2
  98. telegrinder/node/composer.py +40 -75
  99. telegrinder/node/container.py +13 -7
  100. telegrinder/node/either.py +82 -0
  101. telegrinder/node/event.py +20 -31
  102. telegrinder/node/file.py +51 -0
  103. telegrinder/node/me.py +4 -5
  104. telegrinder/node/payload.py +78 -0
  105. telegrinder/node/polymorphic.py +27 -8
  106. telegrinder/node/rule.py +2 -6
  107. telegrinder/node/scope.py +4 -6
  108. telegrinder/node/source.py +37 -21
  109. telegrinder/node/text.py +20 -8
  110. telegrinder/node/tools/generator.py +7 -11
  111. telegrinder/py.typed +0 -0
  112. telegrinder/rules.py +0 -61
  113. telegrinder/tools/__init__.py +97 -38
  114. telegrinder/tools/adapter/__init__.py +19 -0
  115. telegrinder/tools/adapter/abc.py +49 -0
  116. telegrinder/tools/adapter/dataclass.py +56 -0
  117. telegrinder/{bot/rules → tools}/adapter/event.py +8 -10
  118. telegrinder/{bot/rules → tools}/adapter/node.py +8 -10
  119. telegrinder/{bot/rules → tools}/adapter/raw_event.py +2 -2
  120. telegrinder/{bot/rules → tools}/adapter/raw_update.py +2 -2
  121. telegrinder/tools/buttons.py +52 -26
  122. telegrinder/tools/callback_data_serilization/__init__.py +5 -0
  123. telegrinder/tools/callback_data_serilization/abc.py +51 -0
  124. telegrinder/tools/callback_data_serilization/json_ser.py +60 -0
  125. telegrinder/tools/callback_data_serilization/msgpack_ser.py +172 -0
  126. telegrinder/tools/error_handler/abc.py +4 -7
  127. telegrinder/tools/error_handler/error.py +0 -0
  128. telegrinder/tools/error_handler/error_handler.py +34 -48
  129. telegrinder/tools/formatting/__init__.py +57 -37
  130. telegrinder/tools/formatting/deep_links.py +541 -0
  131. telegrinder/tools/formatting/{html.py → html_formatter.py} +51 -79
  132. telegrinder/tools/formatting/spec_html_formats.py +14 -60
  133. telegrinder/tools/functional.py +1 -5
  134. telegrinder/tools/global_context/global_context.py +26 -51
  135. telegrinder/tools/global_context/telegrinder_ctx.py +3 -3
  136. telegrinder/tools/i18n/abc.py +0 -0
  137. telegrinder/tools/i18n/middleware/abc.py +3 -6
  138. telegrinder/tools/input_file_directory.py +30 -0
  139. telegrinder/tools/keyboard.py +9 -9
  140. telegrinder/tools/lifespan.py +105 -0
  141. telegrinder/tools/limited_dict.py +5 -10
  142. telegrinder/tools/loop_wrapper/abc.py +7 -2
  143. telegrinder/tools/loop_wrapper/loop_wrapper.py +40 -95
  144. telegrinder/tools/magic.py +184 -34
  145. telegrinder/tools/state_storage/__init__.py +0 -0
  146. telegrinder/tools/state_storage/abc.py +5 -9
  147. telegrinder/tools/state_storage/memory.py +1 -1
  148. telegrinder/tools/strings.py +13 -0
  149. telegrinder/types/__init__.py +8 -0
  150. telegrinder/types/enums.py +31 -21
  151. telegrinder/types/input_file.py +51 -0
  152. telegrinder/types/methods.py +531 -109
  153. telegrinder/types/objects.py +934 -826
  154. telegrinder/verification_utils.py +0 -2
  155. {telegrinder-0.3.4.post1.dist-info → telegrinder-0.4.0.dist-info}/LICENSE +2 -2
  156. telegrinder-0.4.0.dist-info/METADATA +144 -0
  157. telegrinder-0.4.0.dist-info/RECORD +182 -0
  158. {telegrinder-0.3.4.post1.dist-info → telegrinder-0.4.0.dist-info}/WHEEL +1 -1
  159. telegrinder/bot/rules/adapter/__init__.py +0 -17
  160. telegrinder/bot/rules/adapter/abc.py +0 -31
  161. telegrinder/node/message.py +0 -14
  162. telegrinder/node/update.py +0 -15
  163. telegrinder/tools/formatting/links.py +0 -38
  164. telegrinder/tools/kb_set/__init__.py +0 -4
  165. telegrinder/tools/kb_set/base.py +0 -15
  166. telegrinder/tools/kb_set/yaml.py +0 -63
  167. telegrinder-0.3.4.post1.dist-info/METADATA +0 -110
  168. telegrinder-0.3.4.post1.dist-info/RECORD +0 -165
  169. /telegrinder/{bot/rules → tools}/adapter/errors.py +0 -0
@@ -3,20 +3,18 @@ from fntypes.result import Error, Ok, Result
3
3
 
4
4
  from telegrinder.api.api import API
5
5
  from telegrinder.bot.dispatch.context import Context
6
- from telegrinder.bot.rules.adapter.abc import ABCAdapter, Event
7
- from telegrinder.bot.rules.adapter.errors import AdapterError
8
6
  from telegrinder.msgspec_utils import repr_type
9
7
  from telegrinder.node.composer import NodeSession, compose_nodes
8
+ from telegrinder.tools.adapter.abc import ABCAdapter, Event
9
+ from telegrinder.tools.adapter.errors import AdapterError
10
10
  from telegrinder.types.objects import Update
11
11
 
12
12
  if typing.TYPE_CHECKING:
13
- from telegrinder.node.base import Node
13
+ from telegrinder.node.base import IsNode
14
14
 
15
- Ts = typing.TypeVarTuple("Ts", default=typing.Unpack[tuple[type["Node"], ...]])
16
15
 
17
-
18
- class NodeAdapter(typing.Generic[*Ts], ABCAdapter[Update, Event[tuple[*Ts]]]):
19
- def __init__(self, *nodes: *Ts) -> None:
16
+ class NodeAdapter(ABCAdapter[Update, Event[tuple["IsNode", ...]]]):
17
+ def __init__(self, *nodes: "IsNode") -> None:
20
18
  self.nodes = nodes
21
19
 
22
20
  def __repr__(self) -> str:
@@ -30,9 +28,9 @@ class NodeAdapter(typing.Generic[*Ts], ABCAdapter[Update, Event[tuple[*Ts]]]):
30
28
  api: API,
31
29
  update: Update,
32
30
  context: Context,
33
- ) -> Result[Event[tuple[*Ts]], AdapterError]:
31
+ ) -> Result[Event[tuple[NodeSession, ...]], AdapterError]:
34
32
  result = await compose_nodes(
35
- nodes={str(i): typing.cast(type["Node"], node) for i, node in enumerate(self.nodes)},
33
+ nodes={f"node_{i}": typing.cast("IsNode", node) for i, node in enumerate(self.nodes)},
36
34
  ctx=context,
37
35
  data={Update: update, API: api},
38
36
  )
@@ -40,7 +38,7 @@ class NodeAdapter(typing.Generic[*Ts], ABCAdapter[Update, Event[tuple[*Ts]]]):
40
38
  match result:
41
39
  case Ok(collection):
42
40
  sessions: list[NodeSession] = list(collection.sessions.values())
43
- return Ok(Event(tuple(sessions))) # type: ignore
41
+ return Ok(Event(tuple(sessions)))
44
42
  case Error(err):
45
43
  return Error(AdapterError(f"Couldn't compose nodes, error: {err}."))
46
44
 
@@ -2,9 +2,9 @@ from fntypes.result import Error, Ok, Result
2
2
 
3
3
  from telegrinder.api.api import API
4
4
  from telegrinder.bot.dispatch.context import Context
5
- from telegrinder.bot.rules.adapter.abc import ABCAdapter
6
- from telegrinder.bot.rules.adapter.errors import AdapterError
7
5
  from telegrinder.model import Model
6
+ from telegrinder.tools.adapter.abc import ABCAdapter
7
+ from telegrinder.tools.adapter.errors import AdapterError
8
8
  from telegrinder.types.objects import Update
9
9
 
10
10
 
@@ -3,8 +3,8 @@ from fntypes.result import Ok, Result
3
3
  from telegrinder.api.api import API
4
4
  from telegrinder.bot.cute_types.update import UpdateCute
5
5
  from telegrinder.bot.dispatch.context import Context
6
- from telegrinder.bot.rules.adapter.abc import ABCAdapter
7
- from telegrinder.bot.rules.adapter.errors import AdapterError
6
+ from telegrinder.tools.adapter.abc import ABCAdapter
7
+ from telegrinder.tools.adapter.errors import AdapterError
8
8
  from telegrinder.types.objects import Update
9
9
 
10
10
 
@@ -3,9 +3,10 @@ import typing
3
3
 
4
4
  import msgspec
5
5
 
6
- from telegrinder.msgspec_utils import DataclassInstance, encoder
6
+ from telegrinder.msgspec_utils import encoder
7
7
  from telegrinder.types.objects import (
8
8
  CallbackGame,
9
+ CopyTextButton,
9
10
  KeyboardButtonPollType,
10
11
  KeyboardButtonRequestChat,
11
12
  KeyboardButtonRequestUsers,
@@ -14,20 +15,21 @@ from telegrinder.types.objects import (
14
15
  WebAppInfo,
15
16
  )
16
17
 
17
- KeyboardButton = typing.TypeVar("KeyboardButton", bound="BaseButton")
18
+ from .callback_data_serilization import ABCDataSerializer, JSONSerializer
19
+
20
+ if typing.TYPE_CHECKING:
21
+ from _typeshed import DataclassInstance
22
+
23
+ type CallbackData = str | bytes | dict[str, typing.Any] | DataclassInstance | msgspec.Struct
18
24
 
19
25
 
20
26
  @dataclasses.dataclass
21
27
  class BaseButton:
22
28
  def get_data(self) -> dict[str, typing.Any]:
23
- return {
24
- k: v if k != "callback_data" or isinstance(v, str) else encoder.encode(v)
25
- for k, v in dataclasses.asdict(self).items()
26
- if v is not None
27
- }
29
+ return {k: v for k, v in dataclasses.asdict(self).items() if v is not None}
28
30
 
29
31
 
30
- class RowButtons(typing.Generic[KeyboardButton]):
32
+ class RowButtons[KeyboardButton: BaseButton]:
31
33
  buttons: list[KeyboardButton]
32
34
  auto_row: bool
33
35
 
@@ -39,45 +41,69 @@ class RowButtons(typing.Generic[KeyboardButton]):
39
41
  return [b.get_data() for b in self.buttons]
40
42
 
41
43
 
42
- @dataclasses.dataclass(slots=True)
44
+ @dataclasses.dataclass
43
45
  class Button(BaseButton):
44
46
  text: str
45
47
  request_contact: bool = dataclasses.field(default=False, kw_only=True)
46
48
  request_location: bool = dataclasses.field(default=False, kw_only=True)
47
- request_chat: dict[str, typing.Any] | KeyboardButtonRequestChat | None = dataclasses.field(
48
- default=None, kw_only=True
49
+ request_chat: KeyboardButtonRequestChat | None = dataclasses.field(
50
+ default=None,
51
+ kw_only=True,
49
52
  )
50
- request_user: dict[str, typing.Any] | KeyboardButtonRequestUsers | None = dataclasses.field(
51
- default=None, kw_only=True
53
+ request_user: KeyboardButtonRequestUsers | None = dataclasses.field(default=None, kw_only=True)
54
+ request_poll: KeyboardButtonPollType | None = dataclasses.field(
55
+ default=None,
56
+ kw_only=True,
52
57
  )
53
- request_poll: dict[str, typing.Any] | KeyboardButtonPollType | None = dataclasses.field(
54
- default=None, kw_only=True
55
- )
56
- web_app: dict[str, typing.Any] | WebAppInfo | None = dataclasses.field(default=None, kw_only=True)
58
+ web_app: WebAppInfo | None = dataclasses.field(default=None, kw_only=True)
57
59
 
58
60
 
59
- @dataclasses.dataclass(slots=True)
61
+ @dataclasses.dataclass
60
62
  class InlineButton(BaseButton):
61
63
  text: str
62
64
  url: str | None = dataclasses.field(default=None, kw_only=True)
63
- login_url: dict[str, typing.Any] | LoginUrl | None = dataclasses.field(default=None, kw_only=True)
65
+ login_url: LoginUrl | None = dataclasses.field(default=None, kw_only=True)
64
66
  pay: bool | None = dataclasses.field(default=None, kw_only=True)
65
- callback_data: str | dict[str, typing.Any] | DataclassInstance | msgspec.Struct | None = (
66
- dataclasses.field(default=None, kw_only=True)
67
+ callback_data: CallbackData | None = dataclasses.field(default=None, kw_only=True)
68
+ callback_data_serializer: dataclasses.InitVar[ABCDataSerializer[typing.Any] | None] = dataclasses.field(
69
+ default=None,
70
+ kw_only=True,
67
71
  )
68
- callback_game: dict[str, typing.Any] | CallbackGame | None = dataclasses.field(default=None, kw_only=True)
72
+ callback_game: CallbackGame | None = dataclasses.field(default=None, kw_only=True)
73
+ copy_text: str | CopyTextButton | None = dataclasses.field(default=None, kw_only=True)
69
74
  switch_inline_query: str | None = dataclasses.field(default=None, kw_only=True)
70
75
  switch_inline_query_current_chat: str | None = dataclasses.field(default=None, kw_only=True)
71
- switch_inline_query_chosen_chat: dict[str, typing.Any] | SwitchInlineQueryChosenChat | None = (
72
- dataclasses.field(default=None, kw_only=True)
76
+ switch_inline_query_chosen_chat: SwitchInlineQueryChosenChat | None = dataclasses.field(
77
+ default=None,
78
+ kw_only=True,
73
79
  )
74
- web_app: dict[str, typing.Any] | WebAppInfo | None = dataclasses.field(default=None, kw_only=True)
80
+ web_app: str | WebAppInfo | None = dataclasses.field(default=None, kw_only=True)
81
+
82
+ def __post_init__(self, callback_data_serializer: ABCDataSerializer[typing.Any] | None) -> None:
83
+ if (
84
+ callback_data_serializer is None
85
+ and isinstance(self.callback_data, msgspec.Struct | dict)
86
+ or dataclasses.is_dataclass(self.callback_data)
87
+ ):
88
+ callback_data_serializer = callback_data_serializer or JSONSerializer(
89
+ self.callback_data.__class__,
90
+ )
91
+
92
+ if callback_data_serializer is not None:
93
+ self.callback_data = callback_data_serializer.serialize(self.callback_data)
94
+ elif self.callback_data is not None and not isinstance(self.callback_data, str | bytes):
95
+ self.callback_data = encoder.encode(self.callback_data)
96
+
97
+ if isinstance(self.copy_text, str):
98
+ self.copy_text = CopyTextButton(text=self.copy_text)
99
+
100
+ if isinstance(self.web_app, str):
101
+ self.web_app = WebAppInfo(url=self.web_app)
75
102
 
76
103
 
77
104
  __all__ = (
78
105
  "BaseButton",
79
106
  "Button",
80
- "DataclassInstance",
81
107
  "InlineButton",
82
108
  "RowButtons",
83
109
  )
@@ -0,0 +1,5 @@
1
+ from .abc import ABCDataSerializer
2
+ from .json_ser import JSONSerializer
3
+ from .msgpack_ser import MsgPackSerializer
4
+
5
+ __all__ = ("ABCDataSerializer", "JSONSerializer", "MsgPackSerializer")
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ import typing
5
+ from functools import cached_property
6
+
7
+ import msgspec
8
+ from fntypes.result import Result
9
+
10
+ if typing.TYPE_CHECKING:
11
+ from dataclasses import Field
12
+
13
+ from _typeshed import DataclassInstance
14
+
15
+ type ModelType = DataclassWithIdentKey | ModelWithIdentKey | msgspec.Struct | DataclassInstance
16
+
17
+
18
+ @typing.runtime_checkable
19
+ class DataclassWithIdentKey(typing.Protocol):
20
+ __key__: str
21
+ __dataclass_fields__: typing.ClassVar[dict[str, Field[typing.Any]]]
22
+
23
+
24
+ @typing.runtime_checkable
25
+ class ModelWithIdentKey(typing.Protocol):
26
+ __key__: str
27
+ __struct_fields__: typing.ClassVar[tuple[str, ...]]
28
+ __struct_config__: typing.ClassVar[msgspec.structs.StructConfig]
29
+
30
+
31
+ class ABCDataSerializer[Data](abc.ABC):
32
+ ident_key: str | None = None
33
+
34
+ @abc.abstractmethod
35
+ def __init__(self, data_type: type[Data], /) -> None:
36
+ pass
37
+
38
+ @cached_property
39
+ def key(self) -> str:
40
+ return self.ident_key + "_" if self.ident_key else ""
41
+
42
+ @abc.abstractmethod
43
+ def serialize(self, data: Data) -> str:
44
+ pass
45
+
46
+ @abc.abstractmethod
47
+ def deserialize(self, serialized_data: str) -> Result[Data, str]:
48
+ pass
49
+
50
+
51
+ __all__ = ("ABCDataSerializer",)
@@ -0,0 +1,60 @@
1
+ import typing
2
+
3
+ import msgspec
4
+ from fntypes.result import Error, Ok, Result
5
+
6
+ from telegrinder.modules import json
7
+ from telegrinder.msgspec_utils import decoder
8
+
9
+ from .abc import ABCDataSerializer, ModelType
10
+
11
+ type Json = dict[str, typing.Any] | ModelType
12
+
13
+
14
+ class JSONSerializer[JsonT: Json](ABCDataSerializer[JsonT]):
15
+ @typing.overload
16
+ def __init__(self, model_t: type[JsonT]) -> None: ...
17
+
18
+ @typing.overload
19
+ def __init__(self, model_t: type[JsonT], *, ident_key: str | None = ...) -> None: ...
20
+
21
+ def __init__(
22
+ self,
23
+ model_t: type[JsonT] = dict[str, typing.Any],
24
+ *,
25
+ ident_key: str | None = None,
26
+ ) -> None:
27
+ self.model_t = model_t
28
+ self.ident_key: str | None = ident_key or getattr(model_t, "__key__", None)
29
+
30
+ @classmethod
31
+ def serialize_from_json(cls, data: JsonT, *, ident_key: str | None = None) -> str:
32
+ return cls(data.__class__, ident_key=ident_key).serialize(data)
33
+
34
+ @classmethod
35
+ def deserialize_to_json(cls, serialized_data: str, model_t: type[JsonT]) -> Result[JsonT, str]:
36
+ return cls(model_t).deserialize(serialized_data)
37
+
38
+ def serialize(self, data: JsonT) -> str:
39
+ return self.key + json.dumps(data)
40
+
41
+ def deserialize(self, serialized_data: str) -> Result[JsonT, str]:
42
+ if self.ident_key and not serialized_data.startswith(self.key):
43
+ return Error("Data is not corresponding to key.")
44
+
45
+ data = serialized_data.removeprefix(self.key)
46
+ try:
47
+ data_obj = json.loads(data)
48
+ except (msgspec.ValidationError, msgspec.DecodeError):
49
+ return Error("Cannot decode json.")
50
+
51
+ if not issubclass(self.model_t, dict):
52
+ try:
53
+ return Ok(decoder.convert(data_obj, type=self.model_t))
54
+ except (msgspec.ValidationError, msgspec.DecodeError):
55
+ return Error("Incorrect data.")
56
+
57
+ return Ok(data_obj)
58
+
59
+
60
+ __all__ = ("JSONSerializer",)
@@ -0,0 +1,172 @@
1
+ import base64
2
+ import binascii
3
+ import dataclasses
4
+ import typing
5
+ from collections import deque
6
+ from contextlib import suppress
7
+ from functools import cached_property
8
+
9
+ import msgspec
10
+ from fntypes.result import Error, Ok, Result
11
+
12
+ from telegrinder.msgspec_utils import decoder, encoder, get_class_annotations
13
+
14
+ from .abc import ABCDataSerializer, ModelType
15
+
16
+
17
+ @dataclasses.dataclass(frozen=True, slots=True)
18
+ class ModelParser[Model: ModelType]:
19
+ model_type: type[Model]
20
+
21
+ def _is_model(self, obj: typing.Any, /) -> typing.TypeGuard[ModelType]:
22
+ return dataclasses.is_dataclass(obj) or isinstance(obj, msgspec.Struct)
23
+
24
+ def _model_to_dict(self, model: ModelType) -> dict[str, typing.Any]:
25
+ if dataclasses.is_dataclass(model):
26
+ return dataclasses.asdict(model)
27
+ return msgspec.structs.asdict(model) # type: ignore
28
+
29
+ def _is_union(self, inspected_type: msgspec.inspect.Type, /) -> typing.TypeGuard[msgspec.inspect.UnionType]:
30
+ return isinstance(inspected_type, msgspec.inspect.UnionType)
31
+
32
+ def _is_model_type(
33
+ self,
34
+ inspected_type: msgspec.inspect.Type,
35
+ /,
36
+ ) -> typing.TypeGuard[msgspec.inspect.DataclassType | msgspec.inspect.StructType]:
37
+ return isinstance(inspected_type, msgspec.inspect.DataclassType | msgspec.inspect.StructType)
38
+
39
+ def _is_iter_of_model(
40
+ self, inspected_type: msgspec.inspect.Type, /
41
+ ) -> typing.TypeGuard[msgspec.inspect.ListType]:
42
+ return isinstance(
43
+ inspected_type,
44
+ msgspec.inspect.ListType | msgspec.inspect.SetType | msgspec.inspect.FrozenSetType,
45
+ ) and self._is_model_type(inspected_type.item_type)
46
+
47
+ def _validate_annotation(self, annotation: typing.Any, /) -> tuple[type[ModelType], bool] | None:
48
+ is_iter_of_model = False
49
+ type_args: tuple[msgspec.inspect.Type, ...] | None = None
50
+ inspected_type = msgspec.inspect.type_info(annotation)
51
+
52
+ if self._is_union(inspected_type):
53
+ type_args = inspected_type.types
54
+ elif self._is_iter_of_model(inspected_type):
55
+ type_args = (inspected_type.item_type,)
56
+ is_iter_of_model = True
57
+ elif self._is_model_type(inspected_type):
58
+ type_args = (inspected_type,)
59
+
60
+ if type_args is not None:
61
+ for arg in type_args:
62
+ if self._is_union(arg):
63
+ type_args += arg.types
64
+ if self._is_model_type(arg):
65
+ return (arg.cls, is_iter_of_model)
66
+ if self._is_iter_of_model(arg):
67
+ return (arg.item_type.cls, True) # type: ignore
68
+
69
+ return None
70
+
71
+ def parse(self, model: Model) -> list[typing.Any]:
72
+ """Returns a parsed model as linked list."""
73
+ linked: list[typing.Any] = []
74
+ stack: list[typing.Any] = [(list(self._model_to_dict(model).values()), linked)]
75
+
76
+ while stack:
77
+ current_obj, current = stack.pop()
78
+
79
+ for item in current_obj:
80
+ if self._is_model(item):
81
+ item = self._model_to_dict(item)
82
+ if isinstance(item, dict):
83
+ new_list = []
84
+ current.append(new_list)
85
+ stack.append((list(item.values()), new_list))
86
+ elif isinstance(item, list | tuple):
87
+ new_list = []
88
+ current.append(new_list)
89
+ stack.append((item, new_list))
90
+ else:
91
+ current.append(item)
92
+
93
+ return encoder.to_builtins(linked)
94
+
95
+ def compose(self, linked: list[typing.Any]) -> dict[str, typing.Any]:
96
+ """Compose linked list to dictionary based on the model class annotations `(without validation)`."""
97
+ root_converted_data: dict[str, typing.Any] = {}
98
+ stack: deque[typing.Any] = deque([(linked, self.model_type, root_converted_data)])
99
+
100
+ while stack:
101
+ current_data, current_model, converted_data = stack.pop()
102
+
103
+ for index, (field, annotation) in enumerate(get_class_annotations(current_model).items()):
104
+ obj, model_type, is_iter_of_model = current_data[index], None, False
105
+
106
+ if isinstance(obj, list) and (validated := self._validate_annotation(annotation)):
107
+ model_type, is_iter_of_model = validated
108
+
109
+ if model_type is not None:
110
+ if is_iter_of_model:
111
+ converted_data[field] = []
112
+ for item in obj:
113
+ new_converted_data = {}
114
+ converted_data[field].append(new_converted_data)
115
+ stack.append((item, model_type, new_converted_data))
116
+ else:
117
+ new_converted_data = {}
118
+ converted_data[field] = new_converted_data
119
+ stack.append((obj, model_type, new_converted_data))
120
+ else:
121
+ converted_data[field] = obj
122
+
123
+ return root_converted_data
124
+
125
+
126
+ class MsgPackSerializer[Model: ModelType](ABCDataSerializer[Model]):
127
+ @typing.overload
128
+ def __init__(self, model_t: type[Model], /) -> None: ...
129
+
130
+ @typing.overload
131
+ def __init__(self, model_t: type[Model], /, *, ident_key: str | None = ...) -> None: ...
132
+
133
+ def __init__(self, model_t: type[Model], /, *, ident_key: str | None = None) -> None:
134
+ self.model_t = model_t
135
+ self.ident_key: str | None = ident_key or getattr(model_t, "__key__", None)
136
+ self._model_parser = ModelParser(model_t)
137
+
138
+ @classmethod
139
+ def serialize_from_model(cls, model: Model, *, ident_key: str | None = None) -> str:
140
+ return cls(model.__class__, ident_key=ident_key).serialize(model)
141
+
142
+ @classmethod
143
+ def deserialize_to_json(cls, serialized_data: str, model_t: type[Model]) -> Result[Model, str]:
144
+ return cls(model_t).deserialize(serialized_data)
145
+
146
+ @cached_property
147
+ def key(self) -> bytes:
148
+ if self.ident_key:
149
+ return msgspec.msgpack.encode(super().key)
150
+ return b""
151
+
152
+ def serialize(self, data: Model) -> str:
153
+ return base64.urlsafe_b64encode(
154
+ self.key + msgspec.msgpack.encode(self._model_parser.parse(data), enc_hook=encoder.enc_hook),
155
+ ).decode()
156
+
157
+ def deserialize(self, serialized_data: str) -> Result[Model, str]:
158
+ with suppress(msgspec.DecodeError, msgspec.ValidationError, binascii.Error):
159
+ ser_data = base64.urlsafe_b64decode(serialized_data)
160
+ if self.ident_key and not ser_data.startswith(self.key):
161
+ return Error("Data is not corresponding to key.")
162
+
163
+ data: list[typing.Any] = msgspec.msgpack.decode(
164
+ ser_data.removeprefix(self.key),
165
+ dec_hook=decoder.dec_hook(),
166
+ )
167
+ return Ok(decoder.convert(self._model_parser.compose(data), type=self.model_t))
168
+
169
+ return Error("Incorrect data.")
170
+
171
+
172
+ __all__ = ("MsgPackSerializer",)
@@ -1,16 +1,13 @@
1
1
  import typing
2
2
  from abc import ABC, abstractmethod
3
3
 
4
- from fntypes.result import Result
5
-
6
4
  from telegrinder.api import API
7
5
  from telegrinder.bot.dispatch.context import Context
8
6
 
9
- Event = typing.TypeVar("Event")
10
- Handler = typing.Callable[..., typing.Awaitable[typing.Any]]
7
+ type Handler = typing.Callable[..., typing.Awaitable[typing.Any]]
11
8
 
12
9
 
13
- class ABCErrorHandler(ABC, typing.Generic[Event]):
10
+ class ABCErrorHandler[Event](ABC):
14
11
  @abstractmethod
15
12
  def __call__(
16
13
  self,
@@ -22,11 +19,11 @@ class ABCErrorHandler(ABC, typing.Generic[Event]):
22
19
  @abstractmethod
23
20
  async def run(
24
21
  self,
25
- handler: Handler,
22
+ exception: BaseException,
26
23
  event: Event,
27
24
  api: API,
28
25
  ctx: Context,
29
- ) -> Result[typing.Any, typing.Any]:
26
+ ) -> typing.Any:
30
27
  """Run the error handler."""
31
28
 
32
29
 
File without changes