telegrinder 0.1.dev165__py3-none-any.whl → 0.1.dev167__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 (101) hide show
  1. telegrinder/__init__.py +20 -0
  2. telegrinder/api/abc.py +1 -1
  3. telegrinder/api/api.py +8 -6
  4. telegrinder/api/error.py +2 -3
  5. telegrinder/bot/__init__.py +12 -0
  6. telegrinder/bot/bot.py +2 -2
  7. telegrinder/bot/cute_types/__init__.py +4 -0
  8. telegrinder/bot/cute_types/base.py +10 -10
  9. telegrinder/bot/cute_types/callback_query.py +1 -3
  10. telegrinder/bot/cute_types/chat_join_request.py +65 -0
  11. telegrinder/bot/cute_types/chat_member_updated.py +246 -0
  12. telegrinder/bot/cute_types/message.py +44 -38
  13. telegrinder/bot/cute_types/update.py +5 -4
  14. telegrinder/bot/cute_types/utils.py +40 -20
  15. telegrinder/bot/dispatch/__init__.py +8 -1
  16. telegrinder/bot/dispatch/composition.py +7 -7
  17. telegrinder/bot/dispatch/context.py +9 -10
  18. telegrinder/bot/dispatch/dispatch.py +30 -22
  19. telegrinder/bot/dispatch/handler/abc.py +1 -0
  20. telegrinder/bot/dispatch/handler/func.py +21 -5
  21. telegrinder/bot/dispatch/handler/message_reply.py +2 -3
  22. telegrinder/bot/dispatch/middleware/abc.py +2 -4
  23. telegrinder/bot/dispatch/process.py +4 -3
  24. telegrinder/bot/dispatch/return_manager/__init__.py +1 -1
  25. telegrinder/bot/dispatch/return_manager/abc.py +28 -20
  26. telegrinder/bot/dispatch/return_manager/callback_query.py +4 -2
  27. telegrinder/bot/dispatch/return_manager/inline_query.py +4 -2
  28. telegrinder/bot/dispatch/return_manager/message.py +8 -4
  29. telegrinder/bot/dispatch/view/__init__.py +8 -2
  30. telegrinder/bot/dispatch/view/abc.py +27 -23
  31. telegrinder/bot/dispatch/view/box.py +74 -11
  32. telegrinder/bot/dispatch/view/callback_query.py +1 -3
  33. telegrinder/bot/dispatch/view/chat_join_request.py +17 -0
  34. telegrinder/bot/dispatch/view/chat_member.py +26 -0
  35. telegrinder/bot/dispatch/view/message.py +23 -1
  36. telegrinder/bot/dispatch/view/raw.py +112 -0
  37. telegrinder/bot/dispatch/waiter_machine/machine.py +41 -26
  38. telegrinder/bot/dispatch/waiter_machine/middleware.py +14 -7
  39. telegrinder/bot/dispatch/waiter_machine/short_state.py +10 -7
  40. telegrinder/bot/polling/polling.py +2 -4
  41. telegrinder/bot/rules/__init__.py +20 -12
  42. telegrinder/bot/rules/abc.py +0 -9
  43. telegrinder/bot/rules/adapter/event.py +29 -22
  44. telegrinder/bot/rules/callback_data.py +15 -18
  45. telegrinder/bot/rules/chat_join.py +47 -0
  46. telegrinder/bot/rules/enum_text.py +7 -2
  47. telegrinder/bot/rules/fuzzy.py +1 -2
  48. telegrinder/bot/rules/inline.py +3 -3
  49. telegrinder/bot/rules/is_from.py +39 -51
  50. telegrinder/bot/rules/markup.py +1 -2
  51. telegrinder/bot/rules/mention.py +1 -4
  52. telegrinder/bot/rules/message.py +17 -0
  53. telegrinder/bot/rules/message_entities.py +1 -1
  54. telegrinder/bot/rules/regex.py +1 -2
  55. telegrinder/bot/rules/rule_enum.py +1 -3
  56. telegrinder/bot/rules/start.py +7 -7
  57. telegrinder/bot/rules/text.py +2 -1
  58. telegrinder/bot/rules/update.py +16 -0
  59. telegrinder/bot/scenario/checkbox.py +5 -7
  60. telegrinder/client/aiohttp.py +5 -7
  61. telegrinder/model.py +37 -22
  62. telegrinder/modules.py +15 -33
  63. telegrinder/msgspec_utils.py +34 -35
  64. telegrinder/node/__init__.py +12 -12
  65. telegrinder/node/attachment.py +21 -7
  66. telegrinder/node/base.py +14 -13
  67. telegrinder/node/composer.py +5 -5
  68. telegrinder/node/container.py +1 -1
  69. telegrinder/node/message.py +3 -1
  70. telegrinder/node/rule.py +4 -4
  71. telegrinder/node/source.py +6 -2
  72. telegrinder/node/text.py +3 -1
  73. telegrinder/node/tools/generator.py +1 -1
  74. telegrinder/tools/__init__.py +3 -1
  75. telegrinder/tools/buttons.py +4 -6
  76. telegrinder/tools/error_handler/abc.py +1 -2
  77. telegrinder/tools/error_handler/error.py +3 -6
  78. telegrinder/tools/error_handler/error_handler.py +34 -24
  79. telegrinder/tools/formatting/html.py +9 -5
  80. telegrinder/tools/formatting/links.py +1 -3
  81. telegrinder/tools/formatting/spec_html_formats.py +1 -1
  82. telegrinder/tools/global_context/abc.py +3 -1
  83. telegrinder/tools/global_context/global_context.py +13 -31
  84. telegrinder/tools/global_context/telegrinder_ctx.py +1 -1
  85. telegrinder/tools/i18n/base.py +4 -3
  86. telegrinder/tools/i18n/middleware/base.py +1 -3
  87. telegrinder/tools/i18n/simple.py +1 -3
  88. telegrinder/tools/keyboard.py +1 -1
  89. telegrinder/tools/limited_dict.py +27 -0
  90. telegrinder/tools/loop_wrapper/loop_wrapper.py +18 -14
  91. telegrinder/tools/magic.py +1 -1
  92. telegrinder/types/__init__.py +236 -0
  93. telegrinder/types/enums.py +34 -0
  94. telegrinder/types/methods.py +52 -47
  95. telegrinder/types/objects.py +533 -90
  96. telegrinder/verification_utils.py +4 -1
  97. {telegrinder-0.1.dev165.dist-info → telegrinder-0.1.dev167.dist-info}/METADATA +1 -1
  98. telegrinder-0.1.dev167.dist-info/RECORD +137 -0
  99. telegrinder-0.1.dev165.dist-info/RECORD +0 -128
  100. {telegrinder-0.1.dev165.dist-info → telegrinder-0.1.dev167.dist-info}/LICENSE +0 -0
  101. {telegrinder-0.1.dev165.dist-info → telegrinder-0.1.dev167.dist-info}/WHEEL +0 -0
@@ -15,15 +15,14 @@ else:
15
15
 
16
16
  datetime = type("datetime", (dt,), {})
17
17
 
18
-
19
18
  class OptionMeta(type):
20
19
  def __instancecheck__(cls, __instance: typing.Any) -> bool:
21
20
  return isinstance(__instance, fntypes.option.Some | fntypes.option.Nothing)
22
21
 
23
-
24
22
  class Option(typing.Generic[Value], metaclass=OptionMeta):
25
23
  pass
26
24
 
25
+
27
26
  T = typing.TypeVar("T")
28
27
  Ts = typing.TypeVarTuple("Ts")
29
28
 
@@ -51,13 +50,13 @@ def msgspec_convert(obj: typing.Any, t: type[T]) -> Result[T, msgspec.Validation
51
50
  def option_dec_hook(tp: type[Option[typing.Any]], obj: typing.Any) -> Option[typing.Any]:
52
51
  if obj is None:
53
52
  return Nothing
54
- value_type, = typing.get_args(tp) or (typing.Any,)
53
+ (value_type,) = typing.get_args(tp) or (typing.Any,)
55
54
  return msgspec_convert({"value": obj}, fntypes.option.Some[value_type]).unwrap()
56
55
 
57
56
 
58
57
  def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
59
58
  union_types = typing.get_args(tp)
60
-
59
+
61
60
  if isinstance(obj, dict):
62
61
  struct_fields_match_sums: dict[type[msgspec.Struct], int] = {
63
62
  m: sum(1 for k in obj if k in m.__struct_fields__)
@@ -68,14 +67,20 @@ def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
68
67
  reverse = False
69
68
 
70
69
  if len(set(struct_fields_match_sums.values())) != len(struct_fields_match_sums.values()):
71
- struct_fields_match_sums = {m: len(m.__struct_fields__) for m in struct_fields_match_sums}
70
+ struct_fields_match_sums = {
71
+ m: len(m.__struct_fields__) for m in struct_fields_match_sums
72
+ }
72
73
  reverse = True
73
74
 
74
75
  union_types = (
75
- *sorted(struct_fields_match_sums, key=lambda k: struct_fields_match_sums[k], reverse=reverse),
76
+ *sorted(
77
+ struct_fields_match_sums,
78
+ key=lambda k: struct_fields_match_sums[k],
79
+ reverse=reverse,
80
+ ),
76
81
  *union_types,
77
82
  )
78
-
83
+
79
84
  for t in union_types:
80
85
  match msgspec_convert(obj, t):
81
86
  case Ok(value):
@@ -92,7 +97,7 @@ def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
92
97
  class Decoder:
93
98
  """Class `Decoder` for `msgspec` module with decode hook
94
99
  for objects with the specified type.
95
-
100
+
96
101
  ```
97
102
  import enum
98
103
 
@@ -122,7 +127,7 @@ class Decoder:
122
127
  Variative: variative_dec_hook,
123
128
  datetime: lambda t, obj: t.fromtimestamp(obj),
124
129
  }
125
-
130
+
126
131
  def __repr__(self) -> str:
127
132
  return "<{}: dec_hooks={!r}>".format(
128
133
  self.__class__.__name__,
@@ -132,9 +137,9 @@ class Decoder:
132
137
  def add_dec_hook(self, t: T): # type: ignore
133
138
  def decorator(func: DecHook[T]) -> DecHook[T]:
134
139
  return self.dec_hooks.setdefault(get_origin(t), func) # type: ignore
135
-
140
+
136
141
  return decorator
137
-
142
+
138
143
  def dec_hook(self, tp: type[typing.Any], obj: object) -> object:
139
144
  origin_type = t if isinstance((t := get_origin(tp)), type) else type(t)
140
145
  if origin_type not in self.dec_hooks:
@@ -143,7 +148,7 @@ class Decoder:
143
148
  "You can implement decode hook for this type."
144
149
  )
145
150
  return self.dec_hooks[origin_type](tp, obj)
146
-
151
+
147
152
  def convert(
148
153
  self,
149
154
  obj: object,
@@ -163,14 +168,12 @@ class Decoder:
163
168
  builtin_types=builtin_types,
164
169
  str_keys=str_keys,
165
170
  )
166
-
171
+
167
172
  @typing.overload
168
- def decode(self, buf: str | bytes) -> typing.Any:
169
- ...
170
-
173
+ def decode(self, buf: str | bytes) -> typing.Any: ...
174
+
171
175
  @typing.overload
172
- def decode(self, buf: str | bytes, *, strict: bool = True) -> typing.Any:
173
- ...
176
+ def decode(self, buf: str | bytes, *, strict: bool = True) -> typing.Any: ...
174
177
 
175
178
  @typing.overload
176
179
  def decode(
@@ -179,8 +182,7 @@ class Decoder:
179
182
  *,
180
183
  type: type[T],
181
184
  strict: bool = True,
182
- ) -> T:
183
- ...
185
+ ) -> T: ...
184
186
 
185
187
  def decode(
186
188
  self,
@@ -199,7 +201,7 @@ class Decoder:
199
201
 
200
202
  class Encoder:
201
203
  """Class `Encoder` for `msgspec` module with encode hooks for objects.
202
-
204
+
203
205
  ```
204
206
  from datetime import datetime as dt
205
207
 
@@ -231,9 +233,9 @@ class Encoder:
231
233
  def decorator(func: EncHook[T]) -> EncHook[T]:
232
234
  encode_hook = self.enc_hooks.setdefault(get_origin(t), func)
233
235
  return func if encode_hook is not func else encode_hook
234
-
236
+
235
237
  return decorator
236
-
238
+
237
239
  def enc_hook(self, obj: object) -> object:
238
240
  origin_type = get_origin(obj.__class__)
239
241
  if origin_type not in self.enc_hooks:
@@ -244,16 +246,13 @@ class Encoder:
244
246
  return self.enc_hooks[origin_type](obj)
245
247
 
246
248
  @typing.overload
247
- def encode(self, obj: typing.Any) -> str:
248
- ...
249
-
249
+ def encode(self, obj: typing.Any) -> str: ...
250
+
250
251
  @typing.overload
251
- def encode(self, obj: typing.Any, *, as_str: typing.Literal[True]) -> str:
252
- ...
252
+ def encode(self, obj: typing.Any, *, as_str: typing.Literal[True]) -> str: ...
253
253
 
254
254
  @typing.overload
255
- def encode(self, obj: typing.Any, *, as_str: typing.Literal[False]) -> bytes:
256
- ...
255
+ def encode(self, obj: typing.Any, *, as_str: typing.Literal[False]) -> bytes: ...
257
256
 
258
257
  def encode(self, obj: typing.Any, *, as_str: bool = True) -> str | bytes:
259
258
  buf = msgspec.json.encode(obj, enc_hook=self.enc_hook)
@@ -267,14 +266,14 @@ encoder: typing.Final[Encoder] = Encoder()
267
266
  __all__ = (
268
267
  "Decoder",
269
268
  "Encoder",
270
- "Option",
271
269
  "Nothing",
270
+ "Option",
271
+ "datetime",
272
+ "decoder",
273
+ "encoder",
272
274
  "get_origin",
273
- "repr_type",
274
275
  "msgspec_convert",
275
276
  "option_dec_hook",
277
+ "repr_type",
276
278
  "variative_dec_hook",
277
- "datetime",
278
- "decoder",
279
- "encoder",
280
279
  )
@@ -10,22 +10,22 @@ from .tools import generate
10
10
  from .update import UpdateNode
11
11
 
12
12
  __all__ = (
13
- "Node",
14
- "DataNode",
15
- "ScalarNode",
16
13
  "Attachment",
17
- "Photo",
18
- "Video",
19
- "Text",
20
14
  "Audio",
21
- "UpdateNode",
22
- "compose_node",
23
15
  "ComposeError",
16
+ "ContainerNode",
17
+ "DataNode",
24
18
  "MessageNode",
25
- "Source",
26
- "NodeSession",
19
+ "Node",
27
20
  "NodeCollection",
28
- "ContainerNode",
29
- "generate",
21
+ "NodeSession",
22
+ "Photo",
30
23
  "RuleContext",
24
+ "ScalarNode",
25
+ "Source",
26
+ "Text",
27
+ "UpdateNode",
28
+ "Video",
29
+ "compose_node",
30
+ "generate",
31
31
  )
@@ -15,8 +15,12 @@ class Attachment(DataNode):
15
15
  attachment_type: typing.Literal["audio", "document", "photo", "poll", "video"]
16
16
  _: dataclasses.KW_ONLY
17
17
  audio: Option[telegrinder.types.Audio] = dataclasses.field(default_factory=lambda: Nothing())
18
- document: Option[telegrinder.types.Document] = dataclasses.field(default_factory=lambda: Nothing())
19
- photo: Option[list[telegrinder.types.PhotoSize]] = dataclasses.field(default_factory=lambda: Nothing())
18
+ document: Option[telegrinder.types.Document] = dataclasses.field(
19
+ default_factory=lambda: Nothing()
20
+ )
21
+ photo: Option[list[telegrinder.types.PhotoSize]] = dataclasses.field(
22
+ default_factory=lambda: Nothing()
23
+ )
20
24
  poll: Option[telegrinder.types.Poll] = dataclasses.field(default_factory=lambda: Nothing())
21
25
  video: Option[telegrinder.types.Video] = dataclasses.field(default_factory=lambda: Nothing())
22
26
 
@@ -34,31 +38,41 @@ class Photo(DataNode):
34
38
 
35
39
  @classmethod
36
40
  async def compose(cls, attachment: Attachment) -> typing.Self:
37
- return cls(attachment.photo.expect(ComposeError("Attachment is not an photo")))
41
+ if not attachment.photo:
42
+ raise ComposeError("Attachment is not an photo")
43
+ return cls(attachment.photo.unwrap())
38
44
 
39
45
 
40
46
  class Video(ScalarNode, telegrinder.types.Video):
41
47
  @classmethod
42
48
  async def compose(cls, attachment: Attachment) -> telegrinder.types.Video:
43
- return attachment.video.expect(ComposeError("Attachment is not an video"))
49
+ if not attachment.video:
50
+ raise ComposeError("Attachment is not an video")
51
+ return attachment.video.unwrap()
44
52
 
45
53
 
46
54
  class Audio(ScalarNode, telegrinder.types.Audio):
47
55
  @classmethod
48
56
  async def compose(cls, attachment: Attachment) -> telegrinder.types.Audio:
49
- return attachment.audio.expect(ComposeError("Attachment is not an audio"))
57
+ if not attachment.audio:
58
+ raise ComposeError("Attachment is not an audio")
59
+ return attachment.audio.unwrap()
50
60
 
51
61
 
52
62
  class Document(ScalarNode, telegrinder.types.Document):
53
63
  @classmethod
54
64
  async def compose(cls, attachment: Attachment) -> telegrinder.types.Document:
55
- return attachment.document.expect(ComposeError("Attachment is not an document"))
65
+ if not attachment.document:
66
+ raise ComposeError("Attachment is not an document")
67
+ return attachment.document.unwrap()
56
68
 
57
69
 
58
70
  class Poll(ScalarNode, telegrinder.types.Poll):
59
71
  @classmethod
60
72
  async def compose(cls, attachment: Attachment) -> telegrinder.types.Poll:
61
- return attachment.poll.expect(ComposeError("Attachment is not an poll"))
73
+ if not attachment.poll:
74
+ raise ComposeError("Attachment is not an poll")
75
+ return attachment.poll.unwrap()
62
76
 
63
77
 
64
78
  __all__ = (
telegrinder/node/base.py CHANGED
@@ -2,11 +2,12 @@ import abc
2
2
  import inspect
3
3
  import typing
4
4
 
5
- ComposeResult: typing.TypeAlias = typing.Coroutine[typing.Any, typing.Any, typing.Any] | typing.AsyncGenerator[typing.Any, None]
5
+ ComposeResult: typing.TypeAlias = (
6
+ typing.Coroutine[typing.Any, typing.Any, typing.Any] | typing.AsyncGenerator[typing.Any, None]
7
+ )
6
8
 
7
9
 
8
- class ComposeError(BaseException):
9
- ...
10
+ class ComposeError(BaseException): ...
10
11
 
11
12
 
12
13
  class Node(abc.ABC):
@@ -24,7 +25,7 @@ class Node(abc.ABC):
24
25
  @classmethod
25
26
  def get_sub_nodes(cls) -> dict[str, type[typing.Self]]:
26
27
  parameters = inspect.signature(cls.compose).parameters
27
-
28
+
28
29
  sub_nodes = {}
29
30
  for name, param in parameters.items():
30
31
  if param.annotation is inspect._empty:
@@ -32,11 +33,11 @@ class Node(abc.ABC):
32
33
  node = param.annotation
33
34
  sub_nodes[name] = node
34
35
  return sub_nodes
35
-
36
+
36
37
  @classmethod
37
38
  def as_node(cls) -> type[typing.Self]:
38
39
  return cls
39
-
40
+
40
41
  @classmethod
41
42
  def is_generator(cls) -> bool:
42
43
  return inspect.isasyncgenfunction(cls.compose)
@@ -64,9 +65,9 @@ SCALAR_NODE = type("ScalarNode", (), {"node": "scalar"})
64
65
 
65
66
  if typing.TYPE_CHECKING:
66
67
 
67
- class ScalarNode(ScalarNodeProto, abc.ABC):
68
+ class ScalarNode(ScalarNodeProto, abc.ABC):
68
69
  pass
69
-
70
+
70
71
  else:
71
72
 
72
73
  def create_node(cls, bases, dct):
@@ -75,8 +76,8 @@ else:
75
76
 
76
77
  def create_class(name, bases, dct):
77
78
  return type(
78
- "Scalar",
79
- (SCALAR_NODE,),
79
+ "Scalar",
80
+ (SCALAR_NODE,),
80
81
  {"as_node": classmethod(lambda cls: create_node(cls, bases, dct))},
81
82
  )
82
83
 
@@ -85,9 +86,9 @@ else:
85
86
 
86
87
 
87
88
  __all__ = (
88
- "ScalarNode",
89
- "SCALAR_NODE",
89
+ "ComposeError",
90
90
  "DataNode",
91
91
  "Node",
92
- "ComposeError",
92
+ "SCALAR_NODE",
93
+ "ScalarNode",
93
94
  )
@@ -6,7 +6,7 @@ from telegrinder.node import Node
6
6
 
7
7
  class NodeSession:
8
8
  def __init__(
9
- self,
9
+ self,
10
10
  value: typing.Any,
11
11
  subnodes: dict[str, typing.Self],
12
12
  generator: typing.AsyncGenerator[typing.Any, None] | None = None,
@@ -14,11 +14,11 @@ class NodeSession:
14
14
  self.value = value
15
15
  self.subnodes = subnodes
16
16
  self.generator = generator
17
-
17
+
18
18
  async def close(self, with_value: typing.Any | None = None) -> None:
19
19
  for subnode in self.subnodes.values():
20
20
  await subnode.close()
21
-
21
+
22
22
  if self.generator is None:
23
23
  return
24
24
  try:
@@ -36,7 +36,7 @@ class NodeCollection:
36
36
 
37
37
  def values(self) -> dict[str, typing.Any]:
38
38
  return {name: session.value for name, session in self.sessions.items()}
39
-
39
+
40
40
  async def close_all(self, with_value: typing.Any | None = None) -> None:
41
41
  for session in self.sessions.values():
42
42
  await session.close(with_value)
@@ -64,7 +64,7 @@ async def compose_node(
64
64
  else:
65
65
  generator = None
66
66
  value = await _node.compose(**context.values()) # type: ignore
67
-
67
+
68
68
  return NodeSession(value, context.sessions, generator)
69
69
 
70
70
 
@@ -13,7 +13,7 @@ class ContainerNode(Node):
13
13
  @classmethod
14
14
  def get_sub_nodes(cls) -> dict[str, type["Node"]]:
15
15
  return {f"node_{i}": node_t for i, node_t in enumerate(cls.linked_nodes)}
16
-
16
+
17
17
  @classmethod
18
18
  def link_nodes(cls, linked_nodes: list[type[Node]]) -> type["ContainerNode"]:
19
19
  return type("_ContainerNode", (cls,), {"linked_nodes": linked_nodes})
@@ -9,8 +9,10 @@ from .update import UpdateNode
9
9
  class MessageNode(ScalarNode, MessageCute):
10
10
  @classmethod
11
11
  async def compose(cls, update: UpdateNode) -> typing.Self:
12
+ if not update.message:
13
+ raise ComposeError
12
14
  return cls(
13
- **update.message.expect(ComposeError).to_dict(),
15
+ **update.message.unwrap().to_dict(),
14
16
  api=update.api,
15
17
  )
16
18
 
telegrinder/node/rule.py CHANGED
@@ -26,7 +26,7 @@ class RuleContext(dict):
26
26
  @classmethod
27
27
  def as_node(cls) -> type[typing.Self]:
28
28
  return cls
29
-
29
+
30
30
  @classmethod
31
31
  def get_sub_nodes(cls) -> dict:
32
32
  return {"update": UpdateNode}
@@ -37,16 +37,16 @@ class RuleContext(dict):
37
37
 
38
38
  def __new__(cls, *rules: ABCRule) -> type[Node]:
39
39
  return type("_RuleNode", (cls,), {"dataclass": dict, "rules": rules}) # type: ignore
40
-
40
+
41
41
  def __class_getitem__(cls, item: tuple[ABCRule, ...]) -> typing.Self:
42
42
  if not isinstance(item, tuple):
43
43
  item = (item,)
44
44
  return cls(*item)
45
-
45
+
46
46
  @staticmethod
47
47
  def generate_dataclass(cls_: type["RuleContext"]): # noqa: ANN205
48
48
  return dataclasses.dataclass(type(cls_.__name__, (object,), dict(cls_.__dict__)))
49
-
49
+
50
50
  def __init_subclass__(cls) -> None:
51
51
  if cls.__name__ == "_RuleNode":
52
52
  return
@@ -21,9 +21,13 @@ class Source(DataNode):
21
21
  chat=message.chat,
22
22
  thread_id=message.message_thread_id.unwrap_or_none(),
23
23
  )
24
-
24
+
25
25
  async def send(self, text: str) -> Message:
26
- result = await self.api.send_message(self.chat.id, message_thread_id=self.thread_id, text=text)
26
+ result = await self.api.send_message(
27
+ chat_id=self.chat.id,
28
+ message_thread_id=self.thread_id,
29
+ text=text,
30
+ )
27
31
  return result.unwrap()
28
32
 
29
33
 
telegrinder/node/text.py CHANGED
@@ -7,7 +7,9 @@ from .message import MessageNode
7
7
  class Text(ScalarNode, str):
8
8
  @classmethod
9
9
  async def compose(cls, message: MessageNode) -> typing.Self:
10
- return cls(message.text.expect(ComposeError("Message has no text")))
10
+ if not message.text:
11
+ raise ComposeError("Message has no text")
12
+ return cls(message.text.unwrap())
11
13
 
12
14
 
13
15
  __all__ = ("Text",)
@@ -20,7 +20,7 @@ def error_on_none(value: T | None) -> T:
20
20
 
21
21
 
22
22
  def generate(
23
- subnodes: tuple[type[Node], ...],
23
+ subnodes: tuple[type[Node], ...],
24
24
  func: typing.Callable[..., typing.Any],
25
25
  casts: tuple[typing.Callable, ...] = (cast_false_to_none, error_on_none),
26
26
  ) -> type[ContainerNode]:
@@ -66,6 +66,7 @@ from .keyboard import (
66
66
  Keyboard,
67
67
  RowButtons,
68
68
  )
69
+ from .limited_dict import LimitedDict
69
70
  from .loop_wrapper import ABCLoopWrapper, DelayedTask, Lifespan, LoopWrapper
70
71
  from .magic import magic_bundle, resolve_arg_names
71
72
  from .parse_mode import ParseMode
@@ -98,8 +99,9 @@ __all__ = (
98
99
  "Keyboard",
99
100
  "KeyboardSetBase",
100
101
  "KeyboardSetYAML",
101
- "Link",
102
102
  "Lifespan",
103
+ "Link",
104
+ "LimitedDict",
103
105
  "LoopWrapper",
104
106
  "Mention",
105
107
  "ParseMode",
@@ -63,15 +63,13 @@ class InlineButton(BaseButton):
63
63
  url: str | None = None
64
64
  login_url: dict[str, typing.Any] | LoginUrl | None = None
65
65
  pay: bool | None = None
66
- callback_data: (
67
- str | dict[str, typing.Any] | DataclassInstance | msgspec.Struct | None
68
- ) = None
66
+ callback_data: str | dict[str, typing.Any] | DataclassInstance | msgspec.Struct | None = None
69
67
  callback_game: dict[str, typing.Any] | CallbackGame | None = None
70
68
  switch_inline_query: str | None = None
71
69
  switch_inline_query_current_chat: str | None = None
72
- switch_inline_query_chosen_chat: (
73
- dict[str, typing.Any] | SwitchInlineQueryChosenChat | None
74
- ) = None
70
+ switch_inline_query_chosen_chat: dict[str, typing.Any] | SwitchInlineQueryChosenChat | None = (
71
+ None
72
+ )
75
73
  web_app: dict[str, typing.Any] | WebAppInfo | None = None
76
74
 
77
75
 
@@ -4,10 +4,9 @@ from abc import ABC, abstractmethod
4
4
  from fntypes.result import Result
5
5
 
6
6
  from telegrinder.api import ABCAPI
7
- from telegrinder.bot.cute_types import BaseCute
8
7
  from telegrinder.bot.dispatch.context import Context
9
8
 
10
- EventT = typing.TypeVar("EventT", bound=BaseCute)
9
+ EventT = typing.TypeVar("EventT")
11
10
  Handler = typing.Callable[typing.Concatenate[EventT, ...], typing.Awaitable[typing.Any]]
12
11
 
13
12
 
@@ -1,10 +1,7 @@
1
- import typing
2
-
3
-
4
- class CatcherError(TypeError):
5
- def __init__(self, exc: typing.Any, error: str) -> None:
1
+ class CatcherError(BaseException):
2
+ def __init__(self, exc: BaseException, message: str) -> None:
6
3
  self.exc = exc
7
- self.error = error
4
+ self.message = message
8
5
 
9
6
 
10
7
  __all__ = ("CatcherError",)
@@ -57,8 +57,7 @@ class Catcher(typing.Generic[EventT]):
57
57
  ) -> Result[typing.Any, BaseException]:
58
58
  if self.match_exception(exception):
59
59
  logger.debug(
60
- "Catcher {!r} caught an exception in handler {!r}, "
61
- "running catcher...".format(
60
+ "Catcher {!r} caught an exception in handler {!r}, " "running catcher...".format(
62
61
  self.func.__name__,
63
62
  handler_name,
64
63
  )
@@ -71,7 +70,7 @@ class Catcher(typing.Generic[EventT]):
71
70
  )
72
71
  logger.debug("Failed to match exception {!r}!", exception.__class__.__name__)
73
72
  return Error(exception)
74
-
73
+
75
74
  def match_exception(self, exception: BaseException) -> bool:
76
75
  for exc in self.exceptions:
77
76
  if isinstance(exc, type) and type(exception) is exc:
@@ -84,10 +83,11 @@ class Catcher(typing.Generic[EventT]):
84
83
  class ErrorHandler(ABCErrorHandler[EventT]):
85
84
  def __init__(self, catcher: Catcher[EventT] | None = None, /) -> None:
86
85
  self.catcher = catcher
87
-
86
+
88
87
  def __repr__(self) -> str:
89
88
  return (
90
- "<ErrorHandler: exceptions_handled=[{}], catcher={!r}>".format(
89
+ "<{}: exceptions_handled=[{}], catcher={!r}>".format(
90
+ self.__class__.__name__,
91
91
  ", ".join(
92
92
  e.__name__ if isinstance(e, type) else repr(e)
93
93
  for e in self.catcher.exceptions
@@ -95,7 +95,7 @@ class ErrorHandler(ABCErrorHandler[EventT]):
95
95
  self.catcher,
96
96
  )
97
97
  if self.catcher is not None
98
- else "<ErrorHandler: No catcher>"
98
+ else "<{}: No catcher>".format(self.__class__.__name__)
99
99
  )
100
100
 
101
101
  def __call__(
@@ -106,7 +106,8 @@ class ErrorHandler(ABCErrorHandler[EventT]):
106
106
  ignore_errors: bool = False,
107
107
  ):
108
108
  """Register the catcher.
109
- :param logging: Error logging in stderr.
109
+
110
+ :param logging: Logging the result of the catcher at the level 'DEBUG'.
110
111
  :param raise_exception: Raise an exception if the catcher hasn't started.
111
112
  :param ignore_errors: Ignore errors that may occur.
112
113
  """
@@ -121,8 +122,9 @@ class ErrorHandler(ABCErrorHandler[EventT]):
121
122
  ignore_errors=ignore_errors,
122
123
  )
123
124
  return func
125
+
124
126
  return decorator
125
-
127
+
126
128
  async def process(
127
129
  self,
128
130
  handler: Handler[EventT],
@@ -135,23 +137,25 @@ class ErrorHandler(ABCErrorHandler[EventT]):
135
137
  try:
136
138
  return await self.catcher(handler, event, api, ctx)
137
139
  except BaseException as exc:
138
- return Error(CatcherError(
139
- exc,
140
- "Exception {!r} was occurred during the running catcher {!r}.".format(
141
- repr(exc), self.catcher.func.__name__
140
+ return Error(
141
+ CatcherError(
142
+ exc,
143
+ "Exception {} was occurred during the running catcher {!r}.".format(
144
+ repr(exc), self.catcher.func.__name__
145
+ ),
142
146
  )
143
- ))
144
-
145
- def process_catcher_error(self, error: CatcherError) -> Result[None, str]:
147
+ )
148
+
149
+ def process_catcher_error(self, error: CatcherError) -> Result[None, BaseException]:
146
150
  assert self.catcher is not None
147
-
151
+
148
152
  if self.catcher.raise_exception:
149
153
  raise error.exc from None
150
- if not self.catcher.ignore_errors:
151
- return Error(error.error)
152
154
  if self.catcher.logging:
153
- logger.error(error.error)
154
-
155
+ logger.error(error.message)
156
+ if not self.catcher.ignore_errors:
157
+ return Error(error.exc)
158
+
155
159
  return Ok(None)
156
160
 
157
161
  async def run(
@@ -160,16 +164,22 @@ class ErrorHandler(ABCErrorHandler[EventT]):
160
164
  event: EventT,
161
165
  api: ABCAPI,
162
166
  ctx: Context,
163
- ) -> Result[typing.Any, typing.Any]:
167
+ ) -> Result[typing.Any, BaseException]:
164
168
  if not self.catcher:
165
169
  return Ok(await handler(event, **magic_bundle(handler, ctx))) # type: ignore
166
-
170
+
167
171
  match await self.process(handler, event, api, ctx):
168
- case Ok(_) as ok:
172
+ case Ok(value) as ok:
173
+ if self.catcher.logging:
174
+ logger.debug(
175
+ "Catcher {!r} returned a value: {!r}",
176
+ self.catcher.func.__name__,
177
+ value,
178
+ )
169
179
  return ok
170
180
  case Error(exc) as err:
171
181
  if isinstance(exc, CatcherError):
172
- return self.process_catcher_error(exc)
182
+ return self.process_catcher_error(exc)
173
183
  if self.catcher.ignore_errors:
174
184
  return Ok(None)
175
185
  return err