telegrinder 0.1.dev170__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (116) hide show
  1. telegrinder/__init__.py +2 -2
  2. telegrinder/api/__init__.py +1 -2
  3. telegrinder/api/api.py +15 -6
  4. telegrinder/api/error.py +2 -1
  5. telegrinder/api/token.py +36 -0
  6. telegrinder/bot/__init__.py +12 -6
  7. telegrinder/bot/bot.py +18 -6
  8. telegrinder/bot/cute_types/__init__.py +7 -7
  9. telegrinder/bot/cute_types/base.py +122 -20
  10. telegrinder/bot/cute_types/callback_query.py +10 -6
  11. telegrinder/bot/cute_types/chat_join_request.py +4 -5
  12. telegrinder/bot/cute_types/chat_member_updated.py +4 -6
  13. telegrinder/bot/cute_types/inline_query.py +3 -4
  14. telegrinder/bot/cute_types/message.py +32 -21
  15. telegrinder/bot/cute_types/update.py +51 -4
  16. telegrinder/bot/cute_types/utils.py +3 -466
  17. telegrinder/bot/dispatch/__init__.py +10 -11
  18. telegrinder/bot/dispatch/abc.py +8 -5
  19. telegrinder/bot/dispatch/context.py +17 -8
  20. telegrinder/bot/dispatch/dispatch.py +71 -48
  21. telegrinder/bot/dispatch/handler/__init__.py +3 -3
  22. telegrinder/bot/dispatch/handler/abc.py +4 -4
  23. telegrinder/bot/dispatch/handler/func.py +46 -22
  24. telegrinder/bot/dispatch/handler/message_reply.py +6 -7
  25. telegrinder/bot/dispatch/middleware/__init__.py +1 -1
  26. telegrinder/bot/dispatch/middleware/abc.py +2 -2
  27. telegrinder/bot/dispatch/process.py +38 -19
  28. telegrinder/bot/dispatch/return_manager/__init__.py +4 -4
  29. telegrinder/bot/dispatch/return_manager/abc.py +3 -3
  30. telegrinder/bot/dispatch/return_manager/callback_query.py +1 -2
  31. telegrinder/bot/dispatch/return_manager/inline_query.py +1 -2
  32. telegrinder/bot/dispatch/return_manager/message.py +1 -2
  33. telegrinder/bot/dispatch/view/__init__.py +8 -8
  34. telegrinder/bot/dispatch/view/abc.py +18 -16
  35. telegrinder/bot/dispatch/view/box.py +75 -64
  36. telegrinder/bot/dispatch/view/callback_query.py +1 -2
  37. telegrinder/bot/dispatch/view/chat_join_request.py +1 -2
  38. telegrinder/bot/dispatch/view/chat_member.py +16 -2
  39. telegrinder/bot/dispatch/view/inline_query.py +1 -2
  40. telegrinder/bot/dispatch/view/message.py +12 -5
  41. telegrinder/bot/dispatch/view/raw.py +9 -8
  42. telegrinder/bot/dispatch/waiter_machine/__init__.py +3 -3
  43. telegrinder/bot/dispatch/waiter_machine/machine.py +12 -8
  44. telegrinder/bot/dispatch/waiter_machine/middleware.py +1 -1
  45. telegrinder/bot/dispatch/waiter_machine/short_state.py +4 -3
  46. telegrinder/bot/polling/abc.py +1 -1
  47. telegrinder/bot/polling/polling.py +6 -6
  48. telegrinder/bot/rules/__init__.py +20 -20
  49. telegrinder/bot/rules/abc.py +57 -43
  50. telegrinder/bot/rules/adapter/__init__.py +5 -5
  51. telegrinder/bot/rules/adapter/abc.py +6 -3
  52. telegrinder/bot/rules/adapter/errors.py +2 -1
  53. telegrinder/bot/rules/adapter/event.py +28 -13
  54. telegrinder/bot/rules/adapter/node.py +28 -22
  55. telegrinder/bot/rules/adapter/raw_update.py +13 -5
  56. telegrinder/bot/rules/callback_data.py +4 -4
  57. telegrinder/bot/rules/chat_join.py +4 -4
  58. telegrinder/bot/rules/command.py +5 -7
  59. telegrinder/bot/rules/func.py +2 -2
  60. telegrinder/bot/rules/fuzzy.py +1 -1
  61. telegrinder/bot/rules/inline.py +3 -3
  62. telegrinder/bot/rules/integer.py +1 -2
  63. telegrinder/bot/rules/markup.py +5 -3
  64. telegrinder/bot/rules/message_entities.py +2 -2
  65. telegrinder/bot/rules/node.py +2 -2
  66. telegrinder/bot/rules/regex.py +1 -1
  67. telegrinder/bot/rules/rule_enum.py +1 -1
  68. telegrinder/bot/rules/text.py +1 -2
  69. telegrinder/bot/rules/update.py +1 -2
  70. telegrinder/bot/scenario/abc.py +2 -2
  71. telegrinder/bot/scenario/checkbox.py +3 -4
  72. telegrinder/bot/scenario/choice.py +1 -2
  73. telegrinder/model.py +89 -45
  74. telegrinder/modules.py +3 -3
  75. telegrinder/msgspec_utils.py +85 -57
  76. telegrinder/node/__init__.py +17 -10
  77. telegrinder/node/attachment.py +19 -16
  78. telegrinder/node/base.py +46 -22
  79. telegrinder/node/callback_query.py +5 -9
  80. telegrinder/node/command.py +6 -2
  81. telegrinder/node/composer.py +102 -77
  82. telegrinder/node/container.py +3 -3
  83. telegrinder/node/event.py +68 -0
  84. telegrinder/node/me.py +3 -0
  85. telegrinder/node/message.py +6 -10
  86. telegrinder/node/polymorphic.py +15 -10
  87. telegrinder/node/rule.py +20 -6
  88. telegrinder/node/scope.py +9 -1
  89. telegrinder/node/source.py +21 -11
  90. telegrinder/node/text.py +4 -4
  91. telegrinder/node/update.py +7 -4
  92. telegrinder/py.typed +0 -0
  93. telegrinder/rules.py +59 -0
  94. telegrinder/tools/__init__.py +2 -2
  95. telegrinder/tools/buttons.py +5 -10
  96. telegrinder/tools/error_handler/abc.py +2 -2
  97. telegrinder/tools/error_handler/error.py +2 -0
  98. telegrinder/tools/error_handler/error_handler.py +6 -6
  99. telegrinder/tools/formatting/spec_html_formats.py +10 -10
  100. telegrinder/tools/global_context/__init__.py +2 -2
  101. telegrinder/tools/global_context/global_context.py +3 -3
  102. telegrinder/tools/global_context/telegrinder_ctx.py +4 -4
  103. telegrinder/tools/keyboard.py +3 -3
  104. telegrinder/tools/loop_wrapper/loop_wrapper.py +47 -13
  105. telegrinder/tools/magic.py +96 -18
  106. telegrinder/types/__init__.py +1 -0
  107. telegrinder/types/enums.py +2 -0
  108. telegrinder/types/methods.py +91 -15
  109. telegrinder/types/objects.py +49 -24
  110. telegrinder/verification_utils.py +1 -3
  111. {telegrinder-0.1.dev170.dist-info → telegrinder-0.2.0.dist-info}/METADATA +2 -2
  112. telegrinder-0.2.0.dist-info/RECORD +145 -0
  113. telegrinder/api/abc.py +0 -73
  114. telegrinder-0.1.dev170.dist-info/RECORD +0 -143
  115. {telegrinder-0.1.dev170.dist-info → telegrinder-0.2.0.dist-info}/LICENSE +0 -0
  116. {telegrinder-0.1.dev170.dist-info → telegrinder-0.2.0.dist-info}/WHEEL +0 -0
@@ -1,3 +1,4 @@
1
+ import dataclasses
1
2
  import typing
2
3
 
3
4
  import fntypes.option
@@ -9,10 +10,16 @@ if typing.TYPE_CHECKING:
9
10
  from datetime import datetime
10
11
 
11
12
  from fntypes.option import Option
12
- from fntypes.result import Result
13
+
14
+ def get_class_annotations(obj: typing.Any) -> dict[str, type[typing.Any]]: ...
15
+
16
+ def get_type_hints(obj: typing.Any) -> dict[str, type[typing.Any]]: ...
17
+
13
18
  else:
14
19
  from datetime import datetime as dt
15
20
 
21
+ from msgspec._utils import get_class_annotations, get_type_hints
22
+
16
23
  Value = typing.TypeVar("Value")
17
24
  Err = typing.TypeVar("Err")
18
25
 
@@ -22,20 +29,12 @@ else:
22
29
  def __instancecheck__(cls, __instance: typing.Any) -> bool:
23
30
  return isinstance(__instance, fntypes.option.Some | fntypes.option.Nothing)
24
31
 
25
- class ResultMeta(type):
26
- def __instancecheck__(cls, __instance: typing.Any) -> bool:
27
- return isinstance(__instance, fntypes.result.Ok | fntypes.result.Error)
28
32
 
29
33
  class Option(typing.Generic[Value], metaclass=OptionMeta):
30
34
  pass
31
35
 
32
- class Result(typing.Generic[Value, Err], metaclass=ResultMeta):
33
- pass
34
-
35
36
 
36
37
  T = typing.TypeVar("T")
37
- Type = typing.TypeVar("Type", bound=type | typing.Any)
38
- Ts = typing.TypeVarTuple("Ts")
39
38
 
40
39
  DecHook: typing.TypeAlias = typing.Callable[[type[T], typing.Any], typing.Any]
41
40
  EncHook: typing.TypeAlias = typing.Callable[[T], typing.Any]
@@ -47,10 +46,29 @@ def get_origin(t: type[T]) -> type[T]:
47
46
  return typing.cast(T, typing.get_origin(t)) or t
48
47
 
49
48
 
50
- def repr_type(t: type) -> str:
49
+ def repr_type(t: typing.Any) -> str:
51
50
  return getattr(t, "__name__", repr(get_origin(t)))
52
51
 
53
52
 
53
+ def is_common_type(type_: typing.Any) -> typing.TypeGuard[type[typing.Any]]:
54
+ if not isinstance(type_, type):
55
+ return False
56
+ return (
57
+ type_ in (str, int, float, bool, None, Variative)
58
+ or issubclass(type_, msgspec.Struct)
59
+ or hasattr(type_, "__dataclass_fields__")
60
+ )
61
+
62
+
63
+ def type_check(obj: typing.Any, t: typing.Any) -> bool:
64
+ return (
65
+ isinstance(obj, t)
66
+ if isinstance(t, type)
67
+ and issubclass(t, msgspec.Struct)
68
+ else type(obj) in t if isinstance(t, tuple) else type(obj) is t
69
+ )
70
+
71
+
54
72
  def msgspec_convert(obj: typing.Any, t: type[T]) -> Result[T, str]:
55
73
  try:
56
74
  return Ok(decoder.convert(obj, type=t, strict=True))
@@ -63,68 +81,68 @@ def msgspec_convert(obj: typing.Any, t: type[T]) -> Result[T, str]:
63
81
  )
64
82
 
65
83
 
66
- def option_dec_hook(tp: type[Option[typing.Any]], obj: typing.Any) -> Option[typing.Any]:
67
- orig_type = get_origin(tp)
68
- (value_type,) = typing.get_args(tp) or (typing.Any,)
69
-
70
- if obj is None and orig_type in (fntypes.option.Nothing, Option):
71
- return fntypes.option.Nothing()
72
- return fntypes.option.Some(msgspec_convert(obj, value_type).unwrap())
84
+ def msgspec_to_builtins(
85
+ obj: typing.Any,
86
+ *,
87
+ str_keys: bool = False,
88
+ builtin_types: typing.Iterable[type[typing.Any]] | None = None,
89
+ order: typing.Literal["deterministic", "sorted"] | None = None,
90
+ ) -> fntypes.result.Result[typing.Any, msgspec.ValidationError]:
91
+ try:
92
+ return Ok(encoder.to_builtins(**locals()))
93
+ except msgspec.ValidationError as exc:
94
+ return Error(exc)
73
95
 
74
96
 
75
- def result_dec_hook(
76
- tp: type[Result[typing.Any, typing.Any]], obj: typing.Any
77
- ) -> Result[typing.Any, typing.Any]:
78
- if not isinstance(obj, dict):
79
- raise TypeError(f"Cannot parse to Result object of type `{repr_type(type(obj))}`.")
97
+ def option_dec_hook(tp: type[Option[typing.Any]], obj: typing.Any) -> Option[typing.Any]:
98
+ if obj is None:
99
+ return Nothing
80
100
 
81
- orig_type = get_origin(tp)
82
- (first_type, second_type) = (
83
- typing.get_args(tp) + (typing.Any,) if len(typing.get_args(tp)) == 1 else typing.get_args(tp)
84
- ) or (typing.Any, typing.Any)
101
+ (value_type,) = typing.get_args(tp) or (typing.Any,)
102
+ orig_value_type = typing.get_origin(value_type) or value_type
103
+ orig_obj = obj
85
104
 
86
- if orig_type is Ok and "ok" in obj:
87
- return Ok(msgspec_convert(obj["ok"], first_type).unwrap())
105
+ if not isinstance(orig_obj, dict | list) and is_common_type(orig_value_type):
106
+ if orig_value_type is Variative:
107
+ obj = value_type(orig_obj) # type: ignore
108
+ orig_value_type = typing.get_args(value_type)
88
109
 
89
- if orig_type is Error and "error" in obj:
90
- return Error(msgspec_convert(obj["error"], first_type).unwrap())
110
+ if not type_check(orig_obj, orig_value_type):
111
+ raise TypeError(f"Expected `{repr_type(orig_value_type)}`, got `{repr_type(type(orig_obj))}`.")
91
112
 
92
- if orig_type is Result:
93
- match obj:
94
- case {"ok": ok}:
95
- return Ok(msgspec_convert(ok, first_type).unwrap())
96
- case {"error": error}:
97
- return Error(msgspec_convert(error, second_type).unwrap())
113
+ return fntypes.option.Some(obj)
98
114
 
99
- raise msgspec.ValidationError(f"Cannot parse object `{obj!r}` to Result.")
115
+ return fntypes.option.Some(decoder.convert(orig_obj, type=value_type))
100
116
 
101
117
 
102
118
  def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
103
119
  union_types = typing.get_args(tp)
104
120
 
105
121
  if isinstance(obj, dict):
106
- struct_fields_match_sums: dict[type[msgspec.Struct], int] = {
122
+ models_struct_fields: dict[type[msgspec.Struct], int] = {
107
123
  m: sum(1 for k in obj if k in m.__struct_fields__)
108
124
  for m in union_types
109
125
  if issubclass(get_origin(m), msgspec.Struct)
110
126
  }
111
- union_types = tuple(t for t in union_types if t not in struct_fields_match_sums)
127
+ union_types = tuple(t for t in union_types if t not in models_struct_fields)
112
128
  reverse = False
113
129
 
114
- if len(set(struct_fields_match_sums.values())) != len(struct_fields_match_sums.values()):
115
- struct_fields_match_sums = {m: len(m.__struct_fields__) for m in struct_fields_match_sums}
130
+ if len(set(models_struct_fields.values())) != len(models_struct_fields.values()):
131
+ models_struct_fields = {m: len(m.__struct_fields__) for m in models_struct_fields}
116
132
  reverse = True
117
133
 
118
134
  union_types = (
119
135
  *sorted(
120
- struct_fields_match_sums,
121
- key=lambda k: struct_fields_match_sums[k],
136
+ models_struct_fields,
137
+ key=lambda k: models_struct_fields[k],
122
138
  reverse=reverse,
123
139
  ),
124
140
  *union_types,
125
141
  )
126
142
 
127
143
  for t in union_types:
144
+ if not isinstance(obj, dict | list) and is_common_type(t) and type_check(obj, t):
145
+ return tp(obj)
128
146
  match msgspec_convert(obj, t):
129
147
  case Ok(value):
130
148
  return tp(value)
@@ -137,6 +155,11 @@ def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
137
155
  )
138
156
 
139
157
 
158
+ @typing.runtime_checkable
159
+ class DataclassInstance(typing.Protocol):
160
+ __dataclass_fields__: typing.ClassVar[dict[str, dataclasses.Field[typing.Any]]]
161
+
162
+
140
163
  class Decoder:
141
164
  """Class `Decoder` for `msgspec` module with decode hook
142
165
  for objects with the specified type.
@@ -155,7 +178,6 @@ class Decoder:
155
178
  decoder.dec_hooks[dt] = lambda t, timestamp: t.fromtimestamp(timestamp)
156
179
 
157
180
  decoder.dec_hook(dt, 1713354732) #> datetime.datetime(2024, 4, 17, 14, 52, 12)
158
- decoder.dec_hook(int, "123") #> TypeError: Unknown type `int`. You can implement decode hook for this type.
159
181
 
160
182
  decoder.convert("123", type=int, strict=False) #> 123
161
183
  decoder.convert(1, type=Digit) #> <Digit.ONE: 1>
@@ -166,12 +188,9 @@ class Decoder:
166
188
 
167
189
  def __init__(self) -> None:
168
190
  self.dec_hooks: dict[typing.Any, DecHook[typing.Any]] = {
169
- Result: result_dec_hook,
170
191
  Option: option_dec_hook,
171
192
  Variative: variative_dec_hook,
172
193
  datetime: lambda t, obj: t.fromtimestamp(obj),
173
- fntypes.result.Error: result_dec_hook,
174
- fntypes.result.Ok: result_dec_hook,
175
194
  fntypes.option.Some: option_dec_hook,
176
195
  fntypes.option.Nothing: option_dec_hook,
177
196
  }
@@ -250,8 +269,6 @@ class Encoder:
250
269
  encoder.enc_hooks[dt] = lambda d: int(d.timestamp())
251
270
 
252
271
  encoder.enc_hook(dt.now()) #> 1713354732
253
- encoder.enc_hook(123) #> NotImplementedError: Not implemented encode hook for object of type `int`.
254
-
255
272
  encoder.encode({'digit': Digit.ONE}) #> '{"digit":1}'
256
273
  ```
257
274
  """
@@ -260,10 +277,6 @@ class Encoder:
260
277
  self.enc_hooks: dict[typing.Any, EncHook[typing.Any]] = {
261
278
  fntypes.option.Some: lambda opt: opt.value,
262
279
  fntypes.option.Nothing: lambda _: None,
263
- fntypes.result.Ok: lambda ok: {"ok": ok.value},
264
- fntypes.result.Error: lambda err: {
265
- "error": (str(err.error) if isinstance(err.error, BaseException) else err.error)
266
- },
267
280
  Variative: lambda variative: variative.v,
268
281
  datetime: lambda date: int(date.timestamp()),
269
282
  }
@@ -302,6 +315,22 @@ class Encoder:
302
315
  buf = msgspec.json.encode(obj, enc_hook=self.enc_hook)
303
316
  return buf.decode() if as_str else buf
304
317
 
318
+ def to_builtins(
319
+ self,
320
+ obj: typing.Any,
321
+ *,
322
+ str_keys: bool = False,
323
+ builtin_types: typing.Iterable[type[typing.Any]] | None = None,
324
+ order: typing.Literal["deterministic", "sorted"] | None = None,
325
+ ) -> typing.Any:
326
+ return msgspec.to_builtins(
327
+ obj,
328
+ str_keys=str_keys,
329
+ builtin_types=builtin_types,
330
+ enc_hook=self.enc_hook,
331
+ order=order,
332
+ )
333
+
305
334
 
306
335
  decoder: typing.Final[Decoder] = Decoder()
307
336
  encoder: typing.Final[Encoder] = Encoder()
@@ -315,9 +344,8 @@ __all__ = (
315
344
  "datetime",
316
345
  "decoder",
317
346
  "encoder",
318
- "get_origin",
319
347
  "msgspec_convert",
320
- "option_dec_hook",
321
- "repr_type",
322
- "variative_dec_hook",
348
+ "get_class_annotations",
349
+ "get_type_hints",
350
+ "msgspec_to_builtins",
323
351
  )
@@ -1,10 +1,13 @@
1
1
  from .attachment import Attachment, Audio, Photo, Video
2
2
  from .base import ComposeError, DataNode, Node, ScalarNode, is_node
3
+ from .callback_query import CallbackQueryNode
3
4
  from .command import CommandInfo
4
5
  from .composer import Composition, NodeCollection, NodeSession, compose_node, compose_nodes
5
6
  from .container import ContainerNode
7
+ from .event import EventNode
6
8
  from .me import Me
7
9
  from .message import MessageNode
10
+ from .polymorphic import Polymorphic, impl
8
11
  from .rule import RuleChain
9
12
  from .scope import GLOBAL, PER_CALL, PER_EVENT, NodeScope, global_node, per_call, per_event
10
13
  from .source import ChatSource, Source, UserSource
@@ -15,35 +18,39 @@ from .update import UpdateNode
15
18
  __all__ = (
16
19
  "Attachment",
17
20
  "Audio",
21
+ "CallbackQueryNode",
18
22
  "ChatSource",
23
+ "CommandInfo",
19
24
  "ComposeError",
25
+ "Composition",
20
26
  "ContainerNode",
21
27
  "DataNode",
28
+ "EventNode",
29
+ "GLOBAL",
30
+ "Me",
22
31
  "MessageNode",
23
32
  "Node",
24
33
  "NodeCollection",
34
+ "NodeScope",
25
35
  "NodeSession",
36
+ "PER_CALL",
37
+ "PER_EVENT",
26
38
  "Photo",
39
+ "Polymorphic",
27
40
  "RuleChain",
28
41
  "ScalarNode",
29
42
  "Source",
30
43
  "Text",
31
44
  "TextInteger",
32
- "UserSource",
33
45
  "UpdateNode",
46
+ "UserSource",
34
47
  "Video",
35
48
  "compose_node",
49
+ "compose_nodes",
36
50
  "generate_node",
37
- "Composition",
51
+ "global_node",
52
+ "impl",
38
53
  "is_node",
39
- "compose_nodes",
40
- "NodeScope",
41
- "PER_CALL",
42
- "PER_EVENT",
43
54
  "per_call",
44
55
  "per_event",
45
- "CommandInfo",
46
- "GLOBAL",
47
- "global_node",
48
- "Me",
49
56
  )
@@ -1,30 +1,33 @@
1
1
  import dataclasses
2
2
  import typing
3
3
 
4
- from fntypes import Option, Some
4
+ from fntypes.co import Option, Some
5
5
  from fntypes.option import Nothing
6
6
 
7
7
  import telegrinder.types
8
+ from telegrinder.node.base import ComposeError, DataNode, ScalarNode
9
+ from telegrinder.node.message import MessageNode
8
10
 
9
- from .base import ComposeError, DataNode, ScalarNode
10
- from .message import MessageNode
11
11
 
12
-
13
- @dataclasses.dataclass
12
+ @dataclasses.dataclass(slots=True)
14
13
  class Attachment(DataNode):
15
14
  attachment_type: typing.Literal["audio", "document", "photo", "poll", "video"]
16
15
  audio: Option[telegrinder.types.Audio] = dataclasses.field(
17
- default_factory=lambda: Nothing(), kw_only=True
16
+ default_factory=lambda: Nothing(),
17
+ kw_only=True,
18
18
  )
19
19
  document: Option[telegrinder.types.Document] = dataclasses.field(
20
- default_factory=lambda: Nothing(), kw_only=True
20
+ default_factory=lambda: Nothing(),
21
+ kw_only=True,
21
22
  )
22
23
  photo: Option[list[telegrinder.types.PhotoSize]] = dataclasses.field(
23
- default_factory=lambda: Nothing(), kw_only=True
24
+ default_factory=lambda: Nothing(),
25
+ kw_only=True,
24
26
  )
25
27
  poll: Option[telegrinder.types.Poll] = dataclasses.field(default_factory=lambda: Nothing(), kw_only=True)
26
28
  video: Option[telegrinder.types.Video] = dataclasses.field(
27
- default_factory=lambda: Nothing(), kw_only=True
29
+ default_factory=lambda: Nothing(),
30
+ kw_only=True,
28
31
  )
29
32
 
30
33
  @classmethod
@@ -33,17 +36,17 @@ class Attachment(DataNode):
33
36
  match getattr(message, attachment_type, Nothing()):
34
37
  case Some(attachment):
35
38
  return cls(attachment_type, **{attachment_type: Some(attachment)})
36
- return cls.compose_error("No attachment found in message")
39
+ return cls.compose_error("No attachment found in message.")
37
40
 
38
41
 
39
- @dataclasses.dataclass
42
+ @dataclasses.dataclass(slots=True)
40
43
  class Photo(DataNode):
41
44
  sizes: list[telegrinder.types.PhotoSize]
42
45
 
43
46
  @classmethod
44
47
  async def compose(cls, attachment: Attachment) -> typing.Self:
45
48
  if not attachment.photo:
46
- raise ComposeError("Attachment is not an photo")
49
+ raise ComposeError("Attachment is not a photo.")
47
50
  return cls(attachment.photo.unwrap())
48
51
 
49
52
 
@@ -51,7 +54,7 @@ class Video(ScalarNode, telegrinder.types.Video):
51
54
  @classmethod
52
55
  async def compose(cls, attachment: Attachment) -> telegrinder.types.Video:
53
56
  if not attachment.video:
54
- raise ComposeError("Attachment is not an video")
57
+ raise ComposeError("Attachment is not a video.")
55
58
  return attachment.video.unwrap()
56
59
 
57
60
 
@@ -59,7 +62,7 @@ class Audio(ScalarNode, telegrinder.types.Audio):
59
62
  @classmethod
60
63
  async def compose(cls, attachment: Attachment) -> telegrinder.types.Audio:
61
64
  if not attachment.audio:
62
- raise ComposeError("Attachment is not an audio")
65
+ raise ComposeError("Attachment is not an audio.")
63
66
  return attachment.audio.unwrap()
64
67
 
65
68
 
@@ -67,7 +70,7 @@ class Document(ScalarNode, telegrinder.types.Document):
67
70
  @classmethod
68
71
  async def compose(cls, attachment: Attachment) -> telegrinder.types.Document:
69
72
  if not attachment.document:
70
- raise ComposeError("Attachment is not an document")
73
+ raise ComposeError("Attachment is not a document.")
71
74
  return attachment.document.unwrap()
72
75
 
73
76
 
@@ -75,7 +78,7 @@ class Poll(ScalarNode, telegrinder.types.Poll):
75
78
  @classmethod
76
79
  async def compose(cls, attachment: Attachment) -> telegrinder.types.Poll:
77
80
  if not attachment.poll:
78
- raise ComposeError("Attachment is not an poll")
81
+ raise ComposeError("Attachment is not a poll.")
79
82
  return attachment.poll.unwrap()
80
83
 
81
84
 
telegrinder/node/base.py CHANGED
@@ -1,19 +1,52 @@
1
1
  import abc
2
2
  import inspect
3
3
  import typing
4
+ from types import AsyncGeneratorType
4
5
 
5
- from telegrinder.tools.magic import get_annotations
6
+ from telegrinder.node.scope import NodeScope
7
+ from telegrinder.tools.magic import (
8
+ cache_magic_value,
9
+ get_annotations,
10
+ )
6
11
 
7
- from .scope import NodeScope
12
+ ComposeResult: typing.TypeAlias = typing.Awaitable[typing.Any] | typing.AsyncGenerator[typing.Any, None]
13
+
14
+
15
+ def is_node(maybe_node: type[typing.Any]) -> typing.TypeGuard[type["Node"]]:
16
+ maybe_node = typing.get_origin(maybe_node) or maybe_node
17
+ return (
18
+ isinstance(maybe_node, type)
19
+ and issubclass(maybe_node, Node)
20
+ or isinstance(maybe_node, Node)
21
+ or hasattr(maybe_node, "as_node")
22
+ )
23
+
24
+
25
+ @cache_magic_value("__nodes__")
26
+ def get_nodes(function: typing.Callable[..., typing.Any]) -> dict[str, type["Node"]]:
27
+ return {k: v for k, v in get_annotations(function).items() if is_node(v)}
8
28
 
9
- ComposeResult: typing.TypeAlias = (
10
- typing.Coroutine[typing.Any, typing.Any, typing.Any]
11
- | typing.AsyncGenerator[typing.Any, None]
12
- | typing.Any
13
- )
14
29
 
30
+ @cache_magic_value("__is_generator__")
31
+ def is_generator(function: typing.Callable[..., typing.Any]) -> typing.TypeGuard[AsyncGeneratorType[typing.Any, None]]:
32
+ return inspect.isasyncgenfunction(function)
15
33
 
16
- class ComposeError(BaseException): ...
34
+
35
+ def get_node_calc_lst(node: type["Node"]) -> list[type["Node"]]:
36
+ """ Returns flattened list of node types in ordering required to calculate given node. Provides caching for passed node type """
37
+ if calc_lst := getattr(node, "__nodes_calc_lst__", None):
38
+ return calc_lst
39
+ nodes_lst: list[type["Node"]] = []
40
+ annotations = list(node.as_node().get_subnodes().values())
41
+ for node_type in annotations:
42
+ nodes_lst.extend(get_node_calc_lst(node_type))
43
+ calc_lst = [*nodes_lst, node]
44
+ setattr(node, "__nodes_calc_lst__", calc_lst)
45
+ return calc_lst
46
+
47
+
48
+ class ComposeError(BaseException):
49
+ pass
17
50
 
18
51
 
19
52
  class Node(abc.ABC):
@@ -30,8 +63,8 @@ class Node(abc.ABC):
30
63
  raise ComposeError(error)
31
64
 
32
65
  @classmethod
33
- def get_sub_nodes(cls) -> dict[str, type[typing.Self]]:
34
- return get_annotations(cls.compose)
66
+ def get_subnodes(cls) -> dict[str, type["Node"]]:
67
+ return get_nodes(cls.compose)
35
68
 
36
69
  @classmethod
37
70
  def as_node(cls) -> type[typing.Self]:
@@ -39,7 +72,7 @@ class Node(abc.ABC):
39
72
 
40
73
  @classmethod
41
74
  def is_generator(cls) -> bool:
42
- return inspect.isasyncgenfunction(cls.compose)
75
+ return is_generator(cls.compose)
43
76
 
44
77
 
45
78
  class DataNode(Node, abc.ABC):
@@ -68,7 +101,6 @@ if typing.TYPE_CHECKING:
68
101
  pass
69
102
 
70
103
  else:
71
-
72
104
  def create_node(cls, bases, dct):
73
105
  dct.update(cls.__dict__)
74
106
  return type(cls.__name__, bases, dct)
@@ -87,21 +119,13 @@ else:
87
119
  pass
88
120
 
89
121
 
90
- def is_node(maybe_node: type[typing.Any]) -> typing.TypeGuard[type[Node]]:
91
- maybe_node = typing.get_origin(maybe_node) or maybe_node
92
- return (
93
- isinstance(maybe_node, type)
94
- and issubclass(maybe_node, Node)
95
- or isinstance(maybe_node, Node)
96
- or hasattr(maybe_node, "as_node")
97
- )
98
-
99
-
100
122
  __all__ = (
101
123
  "ComposeError",
102
124
  "DataNode",
103
125
  "Node",
104
126
  "SCALAR_NODE",
105
127
  "ScalarNode",
128
+ "ScalarNodeProto",
129
+ "get_nodes",
106
130
  "is_node",
107
131
  )
@@ -1,18 +1,14 @@
1
- from telegrinder.bot.cute_types import CallbackQueryCute
2
-
3
- from .base import ComposeError, ScalarNode
4
- from .update import UpdateNode
1
+ from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
2
+ from telegrinder.node.base import ComposeError, ScalarNode
3
+ from telegrinder.node.update import UpdateNode
5
4
 
6
5
 
7
6
  class CallbackQueryNode(ScalarNode, CallbackQueryCute):
8
7
  @classmethod
9
8
  async def compose(cls, update: UpdateNode) -> CallbackQueryCute:
10
9
  if not update.callback_query:
11
- raise ComposeError
12
- return CallbackQueryCute(
13
- **update.callback_query.unwrap().to_dict(),
14
- api=update.api,
15
- )
10
+ raise ComposeError("Update is not a callback_query.")
11
+ return update.callback_query.unwrap()
16
12
 
17
13
 
18
14
  __all__ = ("CallbackQueryNode",)
@@ -1,3 +1,4 @@
1
+ import typing
1
2
  from dataclasses import dataclass, field
2
3
 
3
4
  from fntypes import Nothing, Option, Some
@@ -16,14 +17,17 @@ def cut_mention(text: str) -> tuple[str, Option[str]]:
16
17
  return left, Some(right) if right else Nothing()
17
18
 
18
19
 
19
- @dataclass
20
+ @dataclass(slots=True)
20
21
  class CommandInfo(DataNode):
21
22
  name: str
22
23
  arguments: str
23
24
  mention: Option[str] = field(default_factory=Nothing)
24
25
 
25
26
  @classmethod
26
- async def compose(cls, text: Text):
27
+ async def compose(cls, text: Text) -> typing.Self:
27
28
  name, arguments = single_split(text, separator=" ")
28
29
  name, mention = cut_mention(name)
29
30
  return cls(name, arguments, mention)
31
+
32
+
33
+ __all__ = ("CommandInfo", "cut_mention", "single_split")