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,12 +3,13 @@ import typing
3
3
  import vbml
4
4
 
5
5
  from telegrinder.bot.dispatch.context import Context
6
- from telegrinder.node.text import Text
6
+ from telegrinder.node.either import Either
7
+ from telegrinder.node.text import Caption, Text
7
8
  from telegrinder.tools.global_context.telegrinder_ctx import TelegrinderContext
8
9
 
9
10
  from .abc import ABCRule
10
11
 
11
- PatternLike: typing.TypeAlias = str | vbml.Pattern
12
+ type PatternLike = str | vbml.Pattern
12
13
  global_ctx: typing.Final[TelegrinderContext] = TelegrinderContext()
13
14
 
14
15
 
@@ -30,13 +31,11 @@ class Markup(ABCRule):
30
31
  if not isinstance(patterns, list):
31
32
  patterns = [patterns]
32
33
  self.patterns = [
33
- vbml.Pattern(pattern, flags=global_ctx.vbml_pattern_flags)
34
- if isinstance(pattern, str)
35
- else pattern
34
+ vbml.Pattern(pattern, flags=global_ctx.vbml_pattern_flags) if isinstance(pattern, str) else pattern
36
35
  for pattern in patterns
37
36
  ]
38
37
 
39
- def check(self, text: Text, ctx: Context) -> bool:
38
+ def check(self, text: Either[Text, Caption], ctx: Context) -> bool:
40
39
  return check_string(self.patterns, text, ctx)
41
40
 
42
41
 
@@ -1,15 +1,13 @@
1
1
  import abc
2
2
  import typing
3
3
 
4
+ from telegrinder.tools.adapter.event import EventAdapter
4
5
  from telegrinder.types.objects import Message as MessageEvent
5
6
 
6
7
  from .abc import ABCRule, CheckResult, Message
7
- from .adapter import EventAdapter
8
8
 
9
9
 
10
- class MessageRule(ABCRule[Message], abc.ABC):
11
- adapter: EventAdapter[Message] = EventAdapter(MessageEvent, Message)
12
-
10
+ class MessageRule(ABCRule[Message], abc.ABC, adapter=EventAdapter(MessageEvent, Message)):
13
11
  @abc.abstractmethod
14
12
  def check(self, *args: typing.Any, **kwargs: typing.Any) -> CheckResult: ...
15
13
 
@@ -1,12 +1,10 @@
1
- import typing
2
-
3
1
  from telegrinder.bot.dispatch.context import Context
4
2
  from telegrinder.types.enums import MessageEntityType
5
3
  from telegrinder.types.objects import MessageEntity
6
4
 
7
5
  from .message import Message, MessageRule
8
6
 
9
- Entity: typing.TypeAlias = str | MessageEntityType
7
+ type Entity = str | MessageEntityType
10
8
 
11
9
 
12
10
  class HasEntities(MessageRule):
@@ -1,23 +1,29 @@
1
1
  import typing
2
2
 
3
3
  from telegrinder.bot.dispatch.context import Context
4
- from telegrinder.node.base import Node
4
+ from telegrinder.node.base import IsNode, NodeType, is_node
5
+ from telegrinder.tools.adapter.node import NodeAdapter
5
6
 
6
7
  from .abc import ABCRule
7
- from .adapter.node import NodeAdapter
8
8
 
9
9
 
10
- class NodeRule(ABCRule[tuple[Node, ...]]):
11
- def __init__(self, *nodes: type[Node] | tuple[str, type[Node]]) -> None:
12
- bindings = [binding if isinstance(binding, tuple) else (None, binding) for binding in nodes]
13
- self.nodes = [binding[1] for binding in bindings]
14
- self.node_keys = [binding[0] for binding in bindings]
10
+ class NodeRule(ABCRule):
11
+ def __init__(self, *nodes: IsNode | tuple[str, IsNode]) -> None:
12
+ self.nodes: list[IsNode] = []
13
+ self.node_keys: list[str | None] = []
14
+
15
+ for binding in nodes:
16
+ node_key, node_t = binding if isinstance(binding, tuple) else (None, binding)
17
+ if not is_node(node_t):
18
+ continue
19
+ self.nodes.append(node_t)
20
+ self.node_keys.append(node_key)
15
21
 
16
22
  @property
17
23
  def adapter(self) -> NodeAdapter:
18
- return NodeAdapter(*self.nodes) # type: ignore
24
+ return NodeAdapter(*self.nodes)
19
25
 
20
- def check(self, resolved_nodes: tuple[Node, ...], ctx: Context) -> typing.Literal[True]:
26
+ def check(self, resolved_nodes: tuple[NodeType, ...], ctx: Context) -> typing.Literal[True]:
21
27
  for i, node in enumerate(resolved_nodes):
22
28
  if key := self.node_keys[i]:
23
29
  ctx[key] = node
@@ -0,0 +1,81 @@
1
+ import typing
2
+ from contextlib import suppress
3
+ from functools import cached_property
4
+
5
+ import msgspec
6
+
7
+ from telegrinder.bot.dispatch.context import Context
8
+ from telegrinder.bot.rules.abc import ABCRule
9
+ from telegrinder.bot.rules.markup import Markup, PatternLike, check_string
10
+ from telegrinder.msgspec_json import loads
11
+ from telegrinder.node.base import Node
12
+ from telegrinder.node.payload import Payload, PayloadData
13
+ from telegrinder.tools.callback_data_serilization.abc import ABCDataSerializer, ModelType
14
+ from telegrinder.tools.callback_data_serilization.json_ser import JSONSerializer
15
+
16
+
17
+ class PayloadRule[Data](ABCRule):
18
+ def __init__(
19
+ self,
20
+ data_type: type[Data],
21
+ serializer: type[ABCDataSerializer[Data]],
22
+ *,
23
+ alias: str | None = None,
24
+ ) -> None:
25
+ self.data_type = data_type
26
+ self.serializer = serializer
27
+ self.alias = alias or "data"
28
+
29
+ @cached_property
30
+ def required_nodes(self) -> dict[str, type[Node]]:
31
+ return {"payload": PayloadData[self.data_type, self.serializer]} # type: ignore
32
+
33
+ def check(self, payload: PayloadData[Data], context: Context) -> typing.Literal[True]:
34
+ context.set(self.alias, payload)
35
+ return True
36
+
37
+
38
+ class PayloadModelRule[Model: ModelType](PayloadRule[Model]):
39
+ def __init__(
40
+ self,
41
+ model_t: type[Model],
42
+ *,
43
+ serializer: type[ABCDataSerializer[Model]] | None = None,
44
+ alias: str | None = None,
45
+ ) -> None:
46
+ super().__init__(model_t, serializer or JSONSerializer, alias=alias or "model")
47
+
48
+
49
+ class PayloadEqRule(ABCRule):
50
+ def __init__(self, payloads: str | list[str], /) -> None:
51
+ self.payloads = [payloads] if isinstance(payloads, str) else payloads
52
+
53
+ def check(self, payload: Payload) -> bool:
54
+ return any(p == payload for p in self.payloads)
55
+
56
+
57
+ class PayloadMarkupRule(ABCRule):
58
+ def __init__(self, pattern: PatternLike | list[PatternLike], /) -> None:
59
+ self.patterns = Markup(pattern).patterns
60
+
61
+ def check(self, payload: Payload, context: Context) -> bool:
62
+ return check_string(self.patterns, payload, context)
63
+
64
+
65
+ class PayloadJsonEqRule(ABCRule):
66
+ def __init__(self, payload: dict[str, typing.Any], /) -> None:
67
+ self.payload = payload
68
+
69
+ def check(self, payload: Payload) -> bool:
70
+ with suppress(msgspec.DecodeError, msgspec.ValidationError):
71
+ return self.payload == loads(payload)
72
+ return False
73
+
74
+
75
+ __all__ = (
76
+ "PayloadEqRule",
77
+ "PayloadJsonEqRule",
78
+ "PayloadMarkupRule",
79
+ "PayloadModelRule",
80
+ "PayloadRule",
81
+ )
@@ -0,0 +1,29 @@
1
+ import abc
2
+ import typing
3
+
4
+ from telegrinder.bot.cute_types.pre_checkout_query import PreCheckoutQueryCute
5
+ from telegrinder.bot.rules.abc import ABCRule, CheckResult
6
+ from telegrinder.tools.adapter.event import EventAdapter
7
+ from telegrinder.types.enums import Currency, UpdateType
8
+
9
+ PreCheckoutQuery: typing.TypeAlias = PreCheckoutQueryCute
10
+
11
+
12
+ class PaymentInvoiceRule(
13
+ ABCRule[PreCheckoutQuery],
14
+ abc.ABC,
15
+ adapter=EventAdapter(UpdateType.PRE_CHECKOUT_QUERY, PreCheckoutQuery),
16
+ ):
17
+ @abc.abstractmethod
18
+ def check(self, *args: typing.Any, **kwargs: typing.Any) -> CheckResult: ...
19
+
20
+
21
+ class PaymentInvoiceCurrency(PaymentInvoiceRule):
22
+ def __init__(self, currency: str | Currency, /) -> None:
23
+ self.currency = currency
24
+
25
+ def check(self, query: PreCheckoutQuery) -> bool:
26
+ return self.currency == query.currency
27
+
28
+
29
+ __all__ = ("PaymentInvoiceCurrency", "PaymentInvoiceRule")
@@ -2,11 +2,12 @@ import re
2
2
  import typing
3
3
 
4
4
  from telegrinder.bot.dispatch.context import Context
5
- from telegrinder.node.text import Text
5
+ from telegrinder.node.either import Either
6
+ from telegrinder.node.text import Caption, Text
6
7
 
7
8
  from .abc import ABCRule
8
9
 
9
- PatternLike: typing.TypeAlias = str | typing.Pattern[str]
10
+ type PatternLike = str | typing.Pattern[str]
10
11
 
11
12
 
12
13
  class Regex(ABCRule):
@@ -18,11 +19,9 @@ class Regex(ABCRule):
18
19
  case str(regex):
19
20
  self.regexp.append(re.compile(regex))
20
21
  case _:
21
- self.regexp.extend(
22
- re.compile(regexp) if isinstance(regexp, str) else regexp for regexp in regexp
23
- )
22
+ self.regexp.extend(re.compile(regexp) if isinstance(regexp, str) else regexp for regexp in regexp)
24
23
 
25
- def check(self, text: Text, ctx: Context) -> bool:
24
+ def check(self, text: Either[Text, Caption], ctx: Context) -> bool:
26
25
  for regexp in self.regexp:
27
26
  response = re.match(regexp, text)
28
27
  if response is not None:
@@ -9,8 +9,6 @@ from telegrinder.node.source import Source
9
9
  if typing.TYPE_CHECKING:
10
10
  from telegrinder.tools.state_storage.abc import ABCStateStorage
11
11
 
12
- Payload = typing.TypeVar("Payload")
13
-
14
12
 
15
13
  class StateMeta(enum.Enum):
16
14
  NO_STATE = enum.auto()
@@ -18,7 +16,7 @@ class StateMeta(enum.Enum):
18
16
 
19
17
 
20
18
  @dataclasses.dataclass(frozen=True, slots=True, repr=False)
21
- class State(ABCRule, typing.Generic[Payload]):
19
+ class State[Payload](ABCRule):
22
20
  storage: "ABCStateStorage[Payload]"
23
21
  key: str | StateMeta | enum.Enum
24
22
 
@@ -9,25 +9,30 @@ from .node import NodeRule
9
9
 
10
10
  class HasText(NodeRule):
11
11
  def __init__(self) -> None:
12
- super().__init__(node.text.Text)
12
+ super().__init__(node.as_node(node.text.Text))
13
+
14
+
15
+ class HasCaption(NodeRule):
16
+ def __init__(self) -> None:
17
+ super().__init__(node.as_node(node.text.Caption))
13
18
 
14
19
 
15
20
  class Text(ABCRule):
16
- def __init__(self, texts: str | list[str], *, ignore_case: bool = False) -> None:
21
+ def __init__(self, texts: str | list[str], /, *, ignore_case: bool = False) -> None:
17
22
  if not isinstance(texts, list):
18
23
  texts = [texts]
19
24
  self.texts = texts if not ignore_case else list(map(str.lower, texts))
20
25
  self.ignore_case = ignore_case
21
26
 
22
- def check(self, text: node.text.Text) -> bool:
27
+ def check(self, text: node.either.Either[node.text.Text, node.text.Caption]) -> bool:
23
28
  return (text if not self.ignore_case else text.lower()) in self.texts
24
29
 
25
30
  @with_caching_translations
26
31
  async def translate(self, translator: ABCTranslator) -> typing.Self:
27
32
  return self.__class__(
28
- texts=[translator.get(text) for text in self.texts],
33
+ [translator.get(text) for text in self.texts],
29
34
  ignore_case=self.ignore_case,
30
35
  )
31
36
 
32
37
 
33
- __all__ = ("HasText", "Text")
38
+ __all__ = ("HasCaption", "HasText", "Text")
File without changes
@@ -7,12 +7,10 @@ if typing.TYPE_CHECKING:
7
7
  from telegrinder.api import API
8
8
  from telegrinder.bot.dispatch.view.abc import ABCStateView
9
9
 
10
- EventT = typing.TypeVar("EventT", bound=BaseCute)
11
10
 
12
-
13
- class ABCScenario(ABC, typing.Generic[EventT]):
11
+ class ABCScenario[Event: BaseCute](ABC):
14
12
  @abstractmethod
15
- def wait(self, api: "API", view: "ABCStateView[EventT]") -> typing.Any:
13
+ def wait(self, api: "API", view: "ABCStateView[Event]") -> typing.Any:
16
14
  pass
17
15
 
18
16
 
@@ -13,18 +13,15 @@ from telegrinder.types.objects import InlineKeyboardMarkup
13
13
 
14
14
  if typing.TYPE_CHECKING:
15
15
  from telegrinder.api.api import API
16
- from telegrinder.bot.dispatch.view.base import BaseStateView
17
16
 
18
- Key = typing.TypeVar("Key", bound=typing.Hashable)
19
17
 
20
-
21
- class ChoiceCode(enum.StrEnum):
18
+ class ChoiceAction(enum.StrEnum):
22
19
  READY = "ready"
23
20
  CANCEL = "cancel"
24
21
 
25
22
 
26
23
  @dataclasses.dataclass(slots=True)
27
- class Choice(typing.Generic[Key]):
24
+ class Choice[Key: typing.Hashable]:
28
25
  key: Key
29
26
  is_picked: bool
30
27
  default_text: str
@@ -85,14 +82,14 @@ class _Checkbox(ABCScenario[CallbackQueryCute]):
85
82
  )
86
83
  kb.row()
87
84
 
88
- kb.add(InlineButton(self.ready, callback_data=self.random_code + "/" + ChoiceCode.READY))
85
+ kb.add(InlineButton(self.ready, callback_data=self.random_code + "/" + ChoiceAction.READY))
89
86
  if self.cancel_text is not None:
90
87
  kb.row()
91
- kb.add(InlineButton(self.cancel_text, callback_data=self.random_code + "/" + ChoiceCode.CANCEL))
88
+ kb.add(InlineButton(self.cancel_text, callback_data=self.random_code + "/" + ChoiceAction.CANCEL))
92
89
 
93
90
  return kb.get_markup()
94
91
 
95
- def add_option(
92
+ def add_option[Key: typing.Hashable](
96
93
  self,
97
94
  key: Key,
98
95
  default_text: str,
@@ -109,9 +106,9 @@ class _Checkbox(ABCScenario[CallbackQueryCute]):
109
106
  code = cb.data.unwrap().replace(self.random_code + "/", "", 1)
110
107
 
111
108
  match code:
112
- case ChoiceCode.READY:
109
+ case ChoiceAction.READY:
113
110
  return False
114
- case ChoiceCode.CANCEL:
111
+ case ChoiceAction.CANCEL:
115
112
  self.choices = []
116
113
  return False
117
114
 
@@ -132,7 +129,6 @@ class _Checkbox(ABCScenario[CallbackQueryCute]):
132
129
  self,
133
130
  hasher: Hasher[CallbackQueryCute, int],
134
131
  api: "API",
135
- view: "BaseStateView[CallbackQueryCute]",
136
132
  ) -> tuple[dict[typing.Hashable, bool], int]:
137
133
  assert len(self.choices) > 0
138
134
  message = (
@@ -145,7 +141,10 @@ class _Checkbox(ABCScenario[CallbackQueryCute]):
145
141
  ).unwrap()
146
142
 
147
143
  while True:
148
- q, _ = await self.waiter_machine.wait(hasher, message.message_id)
144
+ q, _ = await self.waiter_machine.wait(
145
+ hasher,
146
+ data=message.message_id,
147
+ )
149
148
  should_continue = await self.handle(q)
150
149
  await q.answer(self.CALLBACK_ANSWER)
151
150
  if not should_continue:
@@ -159,14 +158,13 @@ class _Checkbox(ABCScenario[CallbackQueryCute]):
159
158
 
160
159
  if typing.TYPE_CHECKING:
161
160
 
162
- class Checkbox(_Checkbox, typing.Generic[Key]):
161
+ class Checkbox[Key: typing.Hashable](_Checkbox):
163
162
  choices: list[Choice[Key]]
164
163
 
165
164
  async def wait(
166
165
  self,
167
166
  hasher: Hasher[CallbackQueryCute, int],
168
167
  api: "API",
169
- view: "BaseStateView[CallbackQueryCute]",
170
168
  ) -> tuple[dict[Key, bool], int]: ...
171
169
 
172
170
  else:
@@ -2,27 +2,24 @@ import typing
2
2
 
3
3
  from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
4
4
  from telegrinder.bot.dispatch.waiter_machine.hasher.hasher import Hasher
5
- from telegrinder.bot.scenario.checkbox import Checkbox, ChoiceCode
5
+ from telegrinder.bot.scenario.checkbox import Checkbox, ChoiceAction
6
6
 
7
7
  if typing.TYPE_CHECKING:
8
8
  from telegrinder.api.api import API
9
9
  from telegrinder.bot.dispatch.view.base import BaseStateView
10
- from telegrinder.bot.scenario.checkbox import Key
11
10
 
12
- class Choice(Checkbox[Key], typing.Generic[Key]):
11
+ class Choice[Key: typing.Hashable](Checkbox[Key]):
13
12
  async def wait(
14
13
  self,
15
14
  hasher: Hasher[CallbackQueryCute, int],
16
15
  api: API,
17
- view: BaseStateView[CallbackQueryCute],
18
16
  ) -> tuple[Key, int]: ...
19
17
 
20
18
  else:
21
-
22
19
  class Choice(Checkbox):
23
20
  async def handle(self, cb):
24
21
  code = cb.data.unwrap().replace(self.random_code + "/", "", 1)
25
- if code == ChoiceCode.READY:
22
+ if code == ChoiceAction.READY:
26
23
  return False
27
24
 
28
25
  for choice in self.choices:
@@ -41,10 +38,10 @@ else:
41
38
 
42
39
  return True
43
40
 
44
- async def wait(self, hasher, api, view):
41
+ async def wait(self, hasher, api):
45
42
  if len(tuple(choice for choice in self.choices if choice.is_picked)) != 1:
46
- raise ValueError("Exactly one choice must be picked")
47
- choices, m_id = await super().wait(hasher, api, view)
43
+ raise ValueError("Exactly one choice must be picked.")
44
+ choices, m_id = await super().wait(hasher, api)
48
45
  return tuple(choices.keys())[tuple(choices.values()).index(True)], m_id
49
46
 
50
47
 
@@ -1,4 +1,12 @@
1
1
  from .abc import ABCClient
2
2
  from .aiohttp import AiohttpClient
3
+ from .form_data import MultipartFormProto, encode_form_data
4
+ from .sonic import AiosonicClient
3
5
 
4
- __all__ = ("ABCClient", "AiohttpClient")
6
+ __all__ = (
7
+ "ABCClient",
8
+ "AiohttpClient",
9
+ "AiosonicClient",
10
+ "MultipartFormProto",
11
+ "encode_form_data",
12
+ )
telegrinder/client/abc.py CHANGED
@@ -1,10 +1,18 @@
1
+ import io
1
2
  import typing
2
3
  from abc import ABC, abstractmethod
3
4
 
5
+ from telegrinder.client.form_data import MultipartFormProto, encode_form_data
6
+
7
+ type Data = dict[str, typing.Any] | MultipartFormProto
8
+
9
+
10
+ class ABCClient[MultipartForm: MultipartFormProto](ABC):
11
+ CONNECTION_TIMEOUT_ERRORS: tuple[type[BaseException], ...] = ()
12
+ CLIENT_CONNECTION_ERRORS: tuple[type[BaseException], ...] = ()
4
13
 
5
- class ABCClient(ABC):
6
14
  @abstractmethod
7
- def __init__(self, *args: typing.Any, **kwargs: typing.Any):
15
+ def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
8
16
  pass
9
17
 
10
18
  @abstractmethod
@@ -12,7 +20,7 @@ class ABCClient(ABC):
12
20
  self,
13
21
  url: str,
14
22
  method: str = "GET",
15
- data: dict[str, typing.Any] | None = None,
23
+ data: Data | None = None,
16
24
  **kwargs: typing.Any,
17
25
  ) -> str:
18
26
  pass
@@ -22,7 +30,7 @@ class ABCClient(ABC):
22
30
  self,
23
31
  url: str,
24
32
  method: str = "GET",
25
- data: dict[str, typing.Any] | None = None,
33
+ data: Data | None = None,
26
34
  **kwargs: typing.Any,
27
35
  ) -> dict[str, typing.Any]:
28
36
  pass
@@ -32,7 +40,7 @@ class ABCClient(ABC):
32
40
  self,
33
41
  url: str,
34
42
  method: str = "GET",
35
- data: dict[str, typing.Any] | None = None,
43
+ data: Data | None = None,
36
44
  **kwargs: typing.Any,
37
45
  ) -> bytes:
38
46
  pass
@@ -42,7 +50,7 @@ class ABCClient(ABC):
42
50
  self,
43
51
  url: str,
44
52
  method: str = "GET",
45
- data: dict[str, typing.Any] | None = None,
53
+ data: Data | None = None,
46
54
  **kwargs: typing.Any,
47
55
  ) -> bytes:
48
56
  pass
@@ -53,12 +61,28 @@ class ABCClient(ABC):
53
61
 
54
62
  @classmethod
55
63
  @abstractmethod
64
+ def multipart_form_factory(cls) -> MultipartForm:
65
+ pass
66
+
67
+ @classmethod
56
68
  def get_form(
57
69
  cls,
70
+ *,
58
71
  data: dict[str, typing.Any],
59
- files: dict[str, tuple[str, bytes]] | None = None,
60
- ) -> typing.Any:
61
- pass
72
+ files: dict[str, tuple[str, typing.Any]] | None = None,
73
+ ) -> MultipartForm:
74
+ multipart_form = cls.multipart_form_factory()
75
+ files = files or {}
76
+
77
+ for k, v in encode_form_data(data, files).items():
78
+ multipart_form.add_field(k, v)
79
+
80
+ for n, (filename, content) in {
81
+ k: (n, io.BytesIO(c) if isinstance(c, bytes) else c) for k, (n, c) in files.items()
82
+ }.items():
83
+ multipart_form.add_field(n, content, filename=filename)
84
+
85
+ return multipart_form
62
86
 
63
87
  async def __aenter__(self) -> typing.Self:
64
88
  return self
@@ -68,8 +92,9 @@ class ABCClient(ABC):
68
92
  exc_type: type[BaseException],
69
93
  exc_val: typing.Any,
70
94
  exc_tb: typing.Any,
71
- ) -> None:
95
+ ) -> bool:
72
96
  await self.close()
97
+ return not bool(exc_val)
73
98
 
74
99
 
75
100
  __all__ = ("ABCClient",)
@@ -11,8 +11,23 @@ from telegrinder.client.abc import ABCClient
11
11
  if typing.TYPE_CHECKING:
12
12
  from aiohttp import ClientResponse
13
13
 
14
+ type Data = dict[str, typing.Any] | aiohttp.formdata.FormData
15
+ type Response = ClientResponse
16
+
17
+
18
+ class AiohttpClient(ABCClient[aiohttp.formdata.FormData]):
19
+ """HTTP client based on `aiohttp` module."""
20
+
21
+ CONNECTION_TIMEOUT_ERRORS = (
22
+ aiohttp.client.ServerConnectionError,
23
+ TimeoutError,
24
+ )
25
+ CLIENT_CONNECTION_ERRORS = (
26
+ aiohttp.client.ClientConnectionError,
27
+ aiohttp.client.ClientConnectorError,
28
+ aiohttp.ClientOSError,
29
+ )
14
30
 
15
- class AiohttpClient(ABCClient):
16
31
  def __init__(
17
32
  self,
18
33
  session: ClientSession | None = None,
@@ -35,15 +50,16 @@ class AiohttpClient(ABCClient):
35
50
  self,
36
51
  url: str,
37
52
  method: str = "GET",
38
- data: dict[str, typing.Any] | None = None,
53
+ data: Data | None = None,
39
54
  **kwargs: typing.Any,
40
- ) -> "ClientResponse":
55
+ ) -> Response:
41
56
  if not self.session:
42
57
  self.session = ClientSession(
43
58
  connector=TCPConnector(ssl=ssl.create_default_context(cafile=certifi.where())),
44
59
  json_serialize=json.dumps,
45
60
  **self.session_params,
46
61
  )
62
+
47
63
  async with self.session.request(
48
64
  url=url,
49
65
  method=method,
@@ -58,7 +74,7 @@ class AiohttpClient(ABCClient):
58
74
  self,
59
75
  url: str,
60
76
  method: str = "GET",
61
- data: dict[str, typing.Any] | None = None,
77
+ data: Data | None = None,
62
78
  **kwargs: typing.Any,
63
79
  ) -> dict[str, typing.Any]:
64
80
  response = await self.request_raw(url, method, data, **kwargs)
@@ -72,7 +88,7 @@ class AiohttpClient(ABCClient):
72
88
  self,
73
89
  url: str,
74
90
  method: str = "GET",
75
- data: dict[str, typing.Any] | aiohttp.FormData | None = None,
91
+ data: Data | None = None,
76
92
  **kwargs: typing.Any,
77
93
  ) -> str:
78
94
  response = await self.request_raw(url, method, data, **kwargs) # type: ignore
@@ -82,48 +98,36 @@ class AiohttpClient(ABCClient):
82
98
  self,
83
99
  url: str,
84
100
  method: str = "GET",
85
- data: dict[str, typing.Any] | aiohttp.FormData | None = None,
101
+ data: Data | None = None,
86
102
  **kwargs: typing.Any,
87
103
  ) -> bytes:
88
104
  response = await self.request_raw(url, method, data, **kwargs) # type: ignore
89
105
  if response._body is None:
90
106
  await response.read()
91
- return response._body
107
+ return response._body or bytes()
92
108
 
93
109
  async def request_content(
94
110
  self,
95
111
  url: str,
96
112
  method: str = "GET",
97
- data: dict[str, typing.Any] | None = None,
113
+ data: Data | None = None,
98
114
  **kwargs: typing.Any,
99
115
  ) -> bytes:
100
116
  response = await self.request_raw(url, method, data, **kwargs)
101
- return response._body
117
+ return response._body or bytes()
102
118
 
103
119
  async def close(self) -> None:
104
120
  if self.session and not self.session.closed:
105
121
  await self.session.close()
106
122
 
107
123
  @classmethod
108
- def get_form(
109
- cls,
110
- data: dict[str, typing.Any],
111
- files: dict[str, tuple[str, bytes]] | None = None,
112
- ) -> aiohttp.formdata.FormData:
113
- files = files or {}
114
- form = aiohttp.formdata.FormData(quote_fields=False)
115
- for k, v in data.items():
116
- form.add_field(k, str(v))
117
-
118
- for n, f in files.items():
119
- form.add_field(n, f[1], filename=f[0])
120
-
121
- return form
124
+ def multipart_form_factory(cls) -> aiohttp.formdata.FormData:
125
+ return aiohttp.formdata.FormData(quote_fields=False)
122
126
 
123
127
  def __del__(self) -> None:
124
128
  if self.session and not self.session.closed:
125
129
  if self.session._connector is not None and self.session._connector_owner:
126
- self.session._connector.close()
130
+ self.session._connector._close()
127
131
  self.session._connector = None
128
132
 
129
133