telegrinder 0.1.dev20__py3-none-any.whl → 0.1.dev158__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 (132) hide show
  1. telegrinder/__init__.py +129 -22
  2. telegrinder/api/__init__.py +11 -2
  3. telegrinder/api/abc.py +25 -9
  4. telegrinder/api/api.py +29 -24
  5. telegrinder/api/error.py +14 -4
  6. telegrinder/api/response.py +11 -7
  7. telegrinder/bot/__init__.py +68 -7
  8. telegrinder/bot/bot.py +30 -24
  9. telegrinder/bot/cute_types/__init__.py +11 -1
  10. telegrinder/bot/cute_types/base.py +47 -0
  11. telegrinder/bot/cute_types/callback_query.py +64 -14
  12. telegrinder/bot/cute_types/inline_query.py +22 -16
  13. telegrinder/bot/cute_types/message.py +145 -53
  14. telegrinder/bot/cute_types/update.py +23 -0
  15. telegrinder/bot/dispatch/__init__.py +56 -3
  16. telegrinder/bot/dispatch/abc.py +9 -7
  17. telegrinder/bot/dispatch/composition.py +74 -0
  18. telegrinder/bot/dispatch/context.py +71 -0
  19. telegrinder/bot/dispatch/dispatch.py +86 -49
  20. telegrinder/bot/dispatch/handler/__init__.py +3 -0
  21. telegrinder/bot/dispatch/handler/abc.py +11 -5
  22. telegrinder/bot/dispatch/handler/func.py +41 -32
  23. telegrinder/bot/dispatch/handler/message_reply.py +46 -0
  24. telegrinder/bot/dispatch/middleware/__init__.py +2 -0
  25. telegrinder/bot/dispatch/middleware/abc.py +10 -4
  26. telegrinder/bot/dispatch/process.py +53 -49
  27. telegrinder/bot/dispatch/return_manager/__init__.py +19 -0
  28. telegrinder/bot/dispatch/return_manager/abc.py +95 -0
  29. telegrinder/bot/dispatch/return_manager/callback_query.py +19 -0
  30. telegrinder/bot/dispatch/return_manager/inline_query.py +14 -0
  31. telegrinder/bot/dispatch/return_manager/message.py +25 -0
  32. telegrinder/bot/dispatch/view/__init__.py +14 -2
  33. telegrinder/bot/dispatch/view/abc.py +121 -2
  34. telegrinder/bot/dispatch/view/box.py +38 -0
  35. telegrinder/bot/dispatch/view/callback_query.py +13 -39
  36. telegrinder/bot/dispatch/view/inline_query.py +11 -39
  37. telegrinder/bot/dispatch/view/message.py +11 -47
  38. telegrinder/bot/dispatch/waiter_machine/__init__.py +9 -0
  39. telegrinder/bot/dispatch/waiter_machine/machine.py +116 -0
  40. telegrinder/bot/dispatch/waiter_machine/middleware.py +76 -0
  41. telegrinder/bot/dispatch/waiter_machine/short_state.py +37 -0
  42. telegrinder/bot/polling/__init__.py +2 -0
  43. telegrinder/bot/polling/abc.py +11 -4
  44. telegrinder/bot/polling/polling.py +89 -40
  45. telegrinder/bot/rules/__init__.py +91 -5
  46. telegrinder/bot/rules/abc.py +81 -63
  47. telegrinder/bot/rules/adapter/__init__.py +11 -0
  48. telegrinder/bot/rules/adapter/abc.py +21 -0
  49. telegrinder/bot/rules/adapter/errors.py +5 -0
  50. telegrinder/bot/rules/adapter/event.py +43 -0
  51. telegrinder/bot/rules/adapter/raw_update.py +24 -0
  52. telegrinder/bot/rules/callback_data.py +159 -38
  53. telegrinder/bot/rules/command.py +116 -0
  54. telegrinder/bot/rules/enum_text.py +28 -0
  55. telegrinder/bot/rules/func.py +17 -17
  56. telegrinder/bot/rules/fuzzy.py +13 -10
  57. telegrinder/bot/rules/inline.py +61 -0
  58. telegrinder/bot/rules/integer.py +12 -7
  59. telegrinder/bot/rules/is_from.py +148 -7
  60. telegrinder/bot/rules/markup.py +21 -18
  61. telegrinder/bot/rules/mention.py +17 -0
  62. telegrinder/bot/rules/message_entities.py +33 -0
  63. telegrinder/bot/rules/regex.py +27 -19
  64. telegrinder/bot/rules/rule_enum.py +74 -0
  65. telegrinder/bot/rules/start.py +25 -13
  66. telegrinder/bot/rules/text.py +23 -14
  67. telegrinder/bot/scenario/__init__.py +2 -0
  68. telegrinder/bot/scenario/abc.py +12 -5
  69. telegrinder/bot/scenario/checkbox.py +48 -30
  70. telegrinder/bot/scenario/choice.py +16 -10
  71. telegrinder/client/__init__.py +2 -0
  72. telegrinder/client/abc.py +8 -21
  73. telegrinder/client/aiohttp.py +30 -21
  74. telegrinder/model.py +68 -37
  75. telegrinder/modules.py +189 -21
  76. telegrinder/msgspec_json.py +14 -0
  77. telegrinder/msgspec_utils.py +207 -0
  78. telegrinder/node/__init__.py +31 -0
  79. telegrinder/node/attachment.py +71 -0
  80. telegrinder/node/base.py +93 -0
  81. telegrinder/node/composer.py +71 -0
  82. telegrinder/node/container.py +22 -0
  83. telegrinder/node/message.py +18 -0
  84. telegrinder/node/rule.py +56 -0
  85. telegrinder/node/source.py +31 -0
  86. telegrinder/node/text.py +13 -0
  87. telegrinder/node/tools/__init__.py +3 -0
  88. telegrinder/node/tools/generator.py +40 -0
  89. telegrinder/node/update.py +12 -0
  90. telegrinder/rules.py +1 -1
  91. telegrinder/tools/__init__.py +165 -4
  92. telegrinder/tools/buttons.py +75 -51
  93. telegrinder/tools/error_handler/__init__.py +8 -0
  94. telegrinder/tools/error_handler/abc.py +30 -0
  95. telegrinder/tools/error_handler/error_handler.py +156 -0
  96. telegrinder/tools/formatting/__init__.py +81 -3
  97. telegrinder/tools/formatting/html.py +283 -37
  98. telegrinder/tools/formatting/links.py +32 -0
  99. telegrinder/tools/formatting/spec_html_formats.py +121 -0
  100. telegrinder/tools/global_context/__init__.py +12 -0
  101. telegrinder/tools/global_context/abc.py +66 -0
  102. telegrinder/tools/global_context/global_context.py +451 -0
  103. telegrinder/tools/global_context/telegrinder_ctx.py +25 -0
  104. telegrinder/tools/i18n/__init__.py +12 -0
  105. telegrinder/tools/i18n/base.py +31 -0
  106. telegrinder/tools/i18n/middleware/__init__.py +3 -0
  107. telegrinder/tools/i18n/middleware/base.py +26 -0
  108. telegrinder/tools/i18n/simple.py +48 -0
  109. telegrinder/tools/inline_query.py +684 -0
  110. telegrinder/tools/kb_set/__init__.py +2 -0
  111. telegrinder/tools/kb_set/base.py +3 -0
  112. telegrinder/tools/kb_set/yaml.py +28 -17
  113. telegrinder/tools/keyboard.py +84 -62
  114. telegrinder/tools/loop_wrapper/__init__.py +4 -0
  115. telegrinder/tools/loop_wrapper/abc.py +18 -0
  116. telegrinder/tools/loop_wrapper/loop_wrapper.py +132 -0
  117. telegrinder/tools/magic.py +48 -23
  118. telegrinder/tools/parse_mode.py +1 -2
  119. telegrinder/types/__init__.py +1 -0
  120. telegrinder/types/enums.py +651 -0
  121. telegrinder/types/methods.py +3920 -1251
  122. telegrinder/types/objects.py +4702 -1718
  123. {telegrinder-0.1.dev20.dist-info → telegrinder-0.1.dev158.dist-info}/LICENSE +2 -1
  124. telegrinder-0.1.dev158.dist-info/METADATA +108 -0
  125. telegrinder-0.1.dev158.dist-info/RECORD +126 -0
  126. {telegrinder-0.1.dev20.dist-info → telegrinder-0.1.dev158.dist-info}/WHEEL +1 -1
  127. telegrinder/bot/dispatch/waiter.py +0 -38
  128. telegrinder/result.py +0 -38
  129. telegrinder/tools/formatting/abc.py +0 -52
  130. telegrinder/tools/formatting/markdown.py +0 -57
  131. telegrinder-0.1.dev20.dist-info/METADATA +0 -22
  132. telegrinder-0.1.dev20.dist-info/RECORD +0 -71
@@ -0,0 +1,207 @@
1
+ import types # noqa: TCH003
2
+ import typing
3
+
4
+ import fntypes.option
5
+ import msgspec
6
+ from fntypes.co import Error, Ok, Result, Variative
7
+
8
+ T = typing.TypeVar("T")
9
+ Ts = typing.TypeVarTuple("Ts")
10
+
11
+ if typing.TYPE_CHECKING:
12
+ from fntypes.option import Option
13
+ else:
14
+
15
+ Value = typing.TypeVar("Value")
16
+
17
+ class OptionMeta(type):
18
+ def __instancecheck__(cls, __instance: typing.Any) -> bool:
19
+ return isinstance(__instance, fntypes.option.Some | fntypes.option.Nothing)
20
+
21
+
22
+ class Option(typing.Generic[Value], metaclass=OptionMeta):
23
+ pass
24
+
25
+ DecHook: typing.TypeAlias = typing.Callable[[type[T], object], object]
26
+ EncHook: typing.TypeAlias = typing.Callable[[T], object]
27
+
28
+ Nothing: typing.Final[fntypes.option.Nothing] = fntypes.option.Nothing()
29
+
30
+
31
+ def get_origin(t: type[T]) -> type[T]:
32
+ return typing.cast(T, typing.get_origin(t)) or t
33
+
34
+
35
+ def repr_type(t: type) -> str:
36
+ return getattr(t, "__name__", repr(get_origin(t)))
37
+
38
+
39
+ def msgspec_convert(obj: typing.Any, t: type[T]) -> Result[T, msgspec.ValidationError]:
40
+ try:
41
+ return Ok(decoder.convert(obj, type=t, strict=True))
42
+ except msgspec.ValidationError as exc:
43
+ return Error(exc)
44
+
45
+
46
+ def option_dec_hook(tp: type["Option[typing.Any]"], obj: typing.Any) -> typing.Any:
47
+ if obj is None:
48
+ return Nothing
49
+ generic_args = typing.get_args(tp)
50
+ value_type: typing.Any | type[typing.Any] = typing.Any if not generic_args else generic_args[0]
51
+ return msgspec_convert({"value": obj}, fntypes.option.Some[value_type]).unwrap()
52
+
53
+
54
+ def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
55
+ union_types = typing.get_args(tp)
56
+
57
+ if isinstance(obj, dict):
58
+ struct_fields_match_sums: dict[type[msgspec.Struct], int] = {
59
+ m: sum(1 for k in obj if k in m.__struct_fields__)
60
+ for m in union_types
61
+ if issubclass(get_origin(m), msgspec.Struct)
62
+ }
63
+ union_types = tuple(t for t in union_types if t not in struct_fields_match_sums)
64
+ reverse = False
65
+
66
+ if len(set(struct_fields_match_sums.values())) != len(struct_fields_match_sums.values()):
67
+ struct_fields_match_sums = {m: len(m.__struct_fields__) for m in struct_fields_match_sums}
68
+ reverse = True
69
+
70
+ union_types = (
71
+ *sorted(struct_fields_match_sums, key=lambda k: struct_fields_match_sums[k], reverse=reverse),
72
+ *union_types,
73
+ )
74
+
75
+ for t in union_types:
76
+ match msgspec_convert(obj, t):
77
+ case Ok(value):
78
+ return tp(value)
79
+
80
+ raise TypeError(
81
+ "Object of type `{}` does not belong to types `{}`".format(
82
+ repr_type(type(obj)),
83
+ " | ".join(map(repr_type, union_types)),
84
+ )
85
+ )
86
+
87
+
88
+ def option_enc_hook(obj: "Option[typing.Any]") -> typing.Any | None:
89
+ return obj.value if isinstance(obj, fntypes.option.Some) else None
90
+
91
+
92
+ def variative_enc_hook(obj: Variative) -> typing.Any:
93
+ return typing.cast(typing.Any, obj.v)
94
+
95
+
96
+ class Decoder:
97
+ def __init__(self) -> None:
98
+ self.dec_hooks: dict[type | types.UnionType, DecHook[typing.Any]] = {
99
+ Option: option_dec_hook,
100
+ Variative: variative_dec_hook,
101
+ }
102
+
103
+ def add_dec_hook(self, tp: type[T]):
104
+ def decorator(func: DecHook[T]) -> DecHook[T]:
105
+ return self.dec_hooks.setdefault(get_origin(tp), func)
106
+
107
+ return decorator
108
+
109
+ def dec_hook(self, tp: type[typing.Any], obj: object) -> object:
110
+ origin_type = t if isinstance((t := get_origin(tp)), type) else type(t)
111
+ if origin_type not in self.dec_hooks:
112
+ raise TypeError(
113
+ f"Unknown type `{repr_type(origin_type)}`. "
114
+ "You can implement decode hook for this type."
115
+ )
116
+ return self.dec_hooks[origin_type](tp, obj)
117
+
118
+ def convert(
119
+ self,
120
+ obj: object,
121
+ *,
122
+ type: type[T] = dict,
123
+ strict: bool = True,
124
+ from_attributes: bool = False,
125
+ builtin_types: typing.Iterable[type] | None = None,
126
+ str_keys: bool = False,
127
+ ) -> T:
128
+ return msgspec.convert(
129
+ obj,
130
+ type,
131
+ strict=strict,
132
+ from_attributes=from_attributes,
133
+ dec_hook=self.dec_hook,
134
+ builtin_types=builtin_types,
135
+ str_keys=str_keys,
136
+ )
137
+
138
+ def decode(
139
+ self,
140
+ buf: str | bytes,
141
+ *,
142
+ type: type[T] = dict,
143
+ strict: bool = True,
144
+ ) -> T:
145
+ return msgspec.json.decode(
146
+ buf,
147
+ type=type,
148
+ strict=strict,
149
+ dec_hook=self.dec_hook,
150
+ )
151
+
152
+
153
+ class Encoder:
154
+ def __init__(self) -> None:
155
+ self.enc_hooks: dict[type, EncHook] = {
156
+ fntypes.option.Some: option_enc_hook,
157
+ fntypes.option.Nothing: option_enc_hook,
158
+ Variative: variative_enc_hook,
159
+ }
160
+
161
+ def add_dec_hook(self, tp: type[T]):
162
+ def decorator(func: EncHook[T]) -> EncHook[T]:
163
+ return self.enc_hooks.setdefault(get_origin(tp), func)
164
+
165
+ return decorator
166
+
167
+ def enc_hook(self, obj: object) -> object:
168
+ origin_type = get_origin(type(obj))
169
+ if origin_type not in self.enc_hooks:
170
+ raise NotImplementedError(
171
+ "Not implemented encode hook for "
172
+ f"object of type `{repr_type(origin_type)}`."
173
+ )
174
+ return self.enc_hooks[origin_type](obj)
175
+
176
+ @typing.overload
177
+ def encode(self, obj: typing.Any) -> str:
178
+ ...
179
+
180
+ @typing.overload
181
+ def encode(self, obj: typing.Any, *, as_str: bool = False) -> bytes:
182
+ ...
183
+
184
+ def encode(self, obj: typing.Any, *, as_str: bool = True) -> str | bytes:
185
+ buf = msgspec.json.encode(obj, enc_hook=self.enc_hook)
186
+ return buf.decode() if as_str else buf
187
+
188
+
189
+ decoder: typing.Final[Decoder] = Decoder()
190
+ encoder: typing.Final[Encoder] = Encoder()
191
+
192
+
193
+ __all__ = (
194
+ "Decoder",
195
+ "Encoder",
196
+ "Option",
197
+ "Nothing",
198
+ "get_origin",
199
+ "repr_type",
200
+ "msgspec_convert",
201
+ "option_dec_hook",
202
+ "option_enc_hook",
203
+ "variative_dec_hook",
204
+ "variative_enc_hook",
205
+ "decoder",
206
+ "encoder",
207
+ )
@@ -0,0 +1,31 @@
1
+ from .attachment import Attachment, Audio, Photo, Video
2
+ from .base import ComposeError, DataNode, Node, ScalarNode
3
+ from .composer import NodeCollection, NodeSession, compose_node
4
+ from .container import ContainerNode
5
+ from .message import MessageNode
6
+ from .rule import RuleContext
7
+ from .source import Source
8
+ from .text import Text
9
+ from .tools import generate
10
+ from .update import UpdateNode
11
+
12
+ __all__ = (
13
+ "Node",
14
+ "DataNode",
15
+ "ScalarNode",
16
+ "Attachment",
17
+ "Photo",
18
+ "Video",
19
+ "Text",
20
+ "Audio",
21
+ "UpdateNode",
22
+ "compose_node",
23
+ "ComposeError",
24
+ "MessageNode",
25
+ "Source",
26
+ "NodeSession",
27
+ "NodeCollection",
28
+ "ContainerNode",
29
+ "generate",
30
+ "RuleContext",
31
+ )
@@ -0,0 +1,71 @@
1
+ import dataclasses
2
+ import typing
3
+
4
+ from fntypes.option import Nothing
5
+
6
+ import telegrinder.types
7
+ from telegrinder.msgspec_utils import Option
8
+
9
+ from .base import ComposeError, DataNode, ScalarNode
10
+ from .message import MessageNode
11
+
12
+
13
+ @dataclasses.dataclass
14
+ class Attachment(DataNode):
15
+ attachment_type: typing.Literal["audio", "document", "photo", "poll", "video"]
16
+ _: dataclasses.KW_ONLY
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())
20
+ poll: Option[telegrinder.types.Poll] = dataclasses.field(default_factory=lambda: Nothing())
21
+ video: Option[telegrinder.types.Video] = dataclasses.field(default_factory=lambda: Nothing())
22
+
23
+ @classmethod
24
+ async def compose(cls, message: MessageNode) -> "Attachment":
25
+ for attachment_type in ("audio", "document", "photo", "poll", "video"):
26
+ if (attachment := getattr(message, attachment_type, None)) is not None:
27
+ return cls(attachment_type, **{attachment_type: attachment})
28
+ return cls.compose_error("No attachment found in message")
29
+
30
+
31
+ @dataclasses.dataclass
32
+ class Photo(DataNode):
33
+ sizes: list[telegrinder.types.PhotoSize]
34
+
35
+ @classmethod
36
+ async def compose(cls, attachment: Attachment) -> typing.Self:
37
+ return cls(attachment.photo.expect(ComposeError("Attachment is not an photo")))
38
+
39
+
40
+ class Video(ScalarNode, telegrinder.types.Video):
41
+ @classmethod
42
+ async def compose(cls, attachment: Attachment) -> telegrinder.types.Video:
43
+ return attachment.video.expect(ComposeError("Attachment is not an video"))
44
+
45
+
46
+ class Audio(ScalarNode, telegrinder.types.Audio):
47
+ @classmethod
48
+ async def compose(cls, attachment: Attachment) -> telegrinder.types.Audio:
49
+ return attachment.audio.expect(ComposeError("Attachment is not an audio"))
50
+
51
+
52
+ class Document(ScalarNode, telegrinder.types.Document):
53
+ @classmethod
54
+ async def compose(cls, attachment: Attachment) -> telegrinder.types.Document:
55
+ return attachment.document.expect(ComposeError("Attachment is not an document"))
56
+
57
+
58
+ class Poll(ScalarNode, telegrinder.types.Poll):
59
+ @classmethod
60
+ async def compose(cls, attachment: Attachment) -> telegrinder.types.Poll:
61
+ return attachment.poll.expect(ComposeError("Attachment is not an poll"))
62
+
63
+
64
+ __all__ = (
65
+ "Attachment",
66
+ "Audio",
67
+ "Document",
68
+ "Photo",
69
+ "Poll",
70
+ "Video",
71
+ )
@@ -0,0 +1,93 @@
1
+ import abc
2
+ import inspect
3
+ import typing
4
+
5
+ ComposeResult: typing.TypeAlias = typing.Coroutine[typing.Any, typing.Any, typing.Any] | typing.AsyncGenerator[typing.Any, None]
6
+
7
+
8
+ class ComposeError(BaseException):
9
+ pass
10
+
11
+
12
+ class Node(abc.ABC):
13
+ node: str = "node"
14
+
15
+ @classmethod
16
+ @abc.abstractmethod
17
+ def compose(cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any) -> ComposeResult:
18
+ pass
19
+
20
+ @classmethod
21
+ def compose_error(cls, error: str | None = None) -> typing.NoReturn:
22
+ raise ComposeError(error)
23
+
24
+ @classmethod
25
+ def get_sub_nodes(cls) -> dict[str, type[typing.Self]]:
26
+ parameters = inspect.signature(cls.compose).parameters
27
+
28
+ sub_nodes = {}
29
+ for name, param in parameters.items():
30
+ if param.annotation is inspect._empty:
31
+ continue
32
+ node = param.annotation
33
+ sub_nodes[name] = node
34
+ return sub_nodes
35
+
36
+ @classmethod
37
+ def as_node(cls) -> type[typing.Self]:
38
+ return cls
39
+
40
+ @classmethod
41
+ def is_generator(cls) -> bool:
42
+ return inspect.isasyncgenfunction(cls.compose)
43
+
44
+
45
+ class DataNode(Node, abc.ABC):
46
+ node = "data"
47
+
48
+ @typing.dataclass_transform()
49
+ @classmethod
50
+ @abc.abstractmethod
51
+ async def compose(cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any) -> ComposeResult:
52
+ pass
53
+
54
+
55
+ class ScalarNodeProto(Node, abc.ABC):
56
+ @classmethod
57
+ @abc.abstractmethod
58
+ async def compose(cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any) -> ComposeResult:
59
+ pass
60
+
61
+
62
+ SCALAR_NODE = type("ScalarNode", (), {"node": "scalar"})
63
+
64
+
65
+ if typing.TYPE_CHECKING:
66
+
67
+ class ScalarNode(ScalarNodeProto, abc.ABC):
68
+ pass
69
+
70
+ else:
71
+
72
+ def create_node(cls, bases, dct):
73
+ dct.update(cls.__dict__)
74
+ return type(cls.__name__, bases, dct)
75
+
76
+ def create_class(name, bases, dct):
77
+ return type(
78
+ "Scalar",
79
+ (SCALAR_NODE,),
80
+ {"as_node": classmethod(lambda cls: create_node(cls, bases, dct))},
81
+ )
82
+
83
+ class ScalarNode(ScalarNodeProto, abc.ABC, metaclass=create_class):
84
+ pass
85
+
86
+
87
+ __all__ = (
88
+ "ScalarNode",
89
+ "SCALAR_NODE",
90
+ "DataNode",
91
+ "Node",
92
+ "ComposeError",
93
+ )
@@ -0,0 +1,71 @@
1
+ import typing
2
+
3
+ from telegrinder.bot.cute_types import UpdateCute
4
+ from telegrinder.node import Node
5
+
6
+
7
+ class NodeSession:
8
+ def __init__(
9
+ self,
10
+ value: typing.Any,
11
+ subnodes: dict[str, typing.Self],
12
+ generator: typing.AsyncGenerator[typing.Any, None] | None = None,
13
+ ):
14
+ self.value = value
15
+ self.subnodes = subnodes
16
+ self.generator = generator
17
+
18
+ async def close(self, with_value: typing.Any | None = None) -> None:
19
+ for subnode in self.subnodes.values():
20
+ await subnode.close()
21
+
22
+ if self.generator is None:
23
+ return
24
+ try:
25
+ await self.generator.asend(with_value)
26
+ except StopAsyncIteration:
27
+ self.generator = None
28
+
29
+ def __repr__(self) -> str:
30
+ return f"<NodeSession {self.value}" + ("ACTIVE>" if self.generator else ">")
31
+
32
+
33
+ class NodeCollection:
34
+ def __init__(self, sessions: dict[str, NodeSession]) -> None:
35
+ self.sessions = sessions
36
+
37
+ def values(self) -> dict[str, typing.Any]:
38
+ return {name: session.value for name, session in self.sessions.items()}
39
+
40
+ async def close_all(self, with_value: typing.Any | None = None) -> None:
41
+ for session in self.sessions.values():
42
+ await session.close(with_value)
43
+
44
+
45
+ async def compose_node(
46
+ node: type[Node],
47
+ update: UpdateCute,
48
+ ready_context: dict[str, NodeSession] | None = None,
49
+ ) -> NodeSession:
50
+ _node = node.as_node()
51
+ context = NodeCollection(ready_context.copy() if ready_context else {})
52
+
53
+ for name, subnode in _node.get_sub_nodes().items():
54
+ if subnode is UpdateCute:
55
+ context.sessions[name] = NodeSession(update, {})
56
+ else:
57
+ context.sessions[name] = await compose_node(subnode, update)
58
+
59
+ generator: typing.AsyncGenerator | None
60
+
61
+ if _node.is_generator():
62
+ generator = typing.cast(typing.AsyncGenerator, _node.compose(**context.values()))
63
+ value = await generator.asend(None)
64
+ else:
65
+ generator = None
66
+ value = await _node.compose(**context.values()) # type: ignore
67
+
68
+ return NodeSession(value, context.sessions, generator)
69
+
70
+
71
+ __all__ = ("NodeCollection", "NodeSession", "compose_node")
@@ -0,0 +1,22 @@
1
+ import typing
2
+
3
+ from .base import Node
4
+
5
+
6
+ class ContainerNode(Node):
7
+ linked_nodes: typing.ClassVar[list[type[Node]]]
8
+
9
+ @classmethod
10
+ async def compose(cls, **kw) -> tuple["Node", ...]:
11
+ return tuple(t[1] for t in sorted(kw.items(), key=lambda t: t[0]))
12
+
13
+ @classmethod
14
+ def get_sub_nodes(cls) -> dict[str, type["Node"]]:
15
+ return {f"node_{i}": node_t for i, node_t in enumerate(cls.linked_nodes)}
16
+
17
+ @classmethod
18
+ def link_nodes(cls, linked_nodes: list[type[Node]]) -> type["ContainerNode"]:
19
+ return type("_ContainerNode", (cls,), {"linked_nodes": linked_nodes})
20
+
21
+
22
+ __all__ = ("ContainerNode",)
@@ -0,0 +1,18 @@
1
+ import typing
2
+
3
+ from telegrinder.bot.cute_types import MessageCute
4
+
5
+ from .base import ComposeError, ScalarNode
6
+ from .update import UpdateNode
7
+
8
+
9
+ class MessageNode(ScalarNode, MessageCute):
10
+ @classmethod
11
+ async def compose(cls, update: UpdateNode) -> typing.Self:
12
+ return cls(
13
+ **update.message.expect(ComposeError).to_dict(),
14
+ api=update.api,
15
+ )
16
+
17
+
18
+ __all__ = ("MessageNode",)
@@ -0,0 +1,56 @@
1
+ import dataclasses
2
+ import typing
3
+
4
+ from telegrinder.bot.dispatch.context import Context
5
+ from telegrinder.bot.dispatch.process import check_rule
6
+ from telegrinder.bot.rules.abc import ABCRule
7
+ from telegrinder.node.base import ComposeError, Node
8
+ from telegrinder.node.update import UpdateNode
9
+
10
+
11
+ class RuleContext(dict):
12
+ dataclass = dict
13
+ rules: tuple[ABCRule, ...] = ()
14
+
15
+ @classmethod
16
+ async def compose(cls, update: UpdateNode):
17
+ ctx = Context()
18
+ for rule in cls.rules:
19
+ if not await check_rule(update.api, rule, update, ctx):
20
+ raise ComposeError
21
+ try:
22
+ return cls.dataclass(**ctx) # type: ignore
23
+ except Exception as exc:
24
+ raise ComposeError(f"Dataclass validation error: {exc}")
25
+
26
+ @classmethod
27
+ def as_node(cls) -> type[typing.Self]:
28
+ return cls
29
+
30
+ @classmethod
31
+ def get_sub_nodes(cls) -> dict:
32
+ return {"update": UpdateNode}
33
+
34
+ @classmethod
35
+ def is_generator(cls) -> typing.Literal[False]:
36
+ return False
37
+
38
+ def __new__(cls, *rules: ABCRule) -> type[Node]:
39
+ return type("_RuleNode", (cls,), {"dataclass": dict, "rules": rules}) # type: ignore
40
+
41
+ def __class_getitem__(cls, item: tuple[ABCRule, ...]) -> typing.Self:
42
+ if not isinstance(item, tuple):
43
+ item = (item,)
44
+ return cls(*item)
45
+
46
+ @staticmethod
47
+ def generate_dataclass(cls_: type["RuleContext"]): # noqa: ANN205
48
+ return dataclasses.dataclass(type(cls_.__name__, (object,), dict(cls_.__dict__)))
49
+
50
+ def __init_subclass__(cls) -> None:
51
+ if cls.__name__ == "_RuleNode":
52
+ return
53
+ cls.dataclass = cls.generate_dataclass(cls)
54
+
55
+
56
+ __all__ = ("RuleContext",)
@@ -0,0 +1,31 @@
1
+ import dataclasses
2
+ import typing
3
+
4
+ from telegrinder.api import API
5
+ from telegrinder.msgspec_utils import Nothing, Option
6
+ from telegrinder.types import Chat, Message
7
+
8
+ from .base import DataNode
9
+ from .message import MessageNode
10
+
11
+
12
+ @dataclasses.dataclass
13
+ class Source(DataNode):
14
+ api: API
15
+ chat: Chat
16
+ thread_id: Option[int] = dataclasses.field(default_factory=lambda: Nothing)
17
+
18
+ @classmethod
19
+ async def compose(cls, message: MessageNode) -> typing.Self:
20
+ return cls(
21
+ api=message.ctx_api,
22
+ chat=message.chat,
23
+ thread_id=message.message_thread_id,
24
+ )
25
+
26
+ async def send(self, text: str) -> Message:
27
+ result = await self.api.send_message(self.chat.id, message_thread_id=self.thread_id, text=text)
28
+ return result.unwrap()
29
+
30
+
31
+ __all__ = ("Source",)
@@ -0,0 +1,13 @@
1
+ import typing
2
+
3
+ from .base import ComposeError, ScalarNode
4
+ from .message import MessageNode
5
+
6
+
7
+ class Text(ScalarNode, str):
8
+ @classmethod
9
+ async def compose(cls, message: MessageNode) -> typing.Self:
10
+ return cls(message.text.expect(ComposeError("Message has no text")))
11
+
12
+
13
+ __all__ = ("Text",)
@@ -0,0 +1,3 @@
1
+ from .generator import generate
2
+
3
+ __all__ = ("generate",)
@@ -0,0 +1,40 @@
1
+ import inspect
2
+ import typing
3
+
4
+ from telegrinder.node.base import ComposeError, Node
5
+ from telegrinder.node.container import ContainerNode
6
+
7
+ T = typing.TypeVar("T")
8
+
9
+
10
+ def cast_false_to_none(value: T) -> T | None:
11
+ if value is False:
12
+ return None
13
+ return value
14
+
15
+
16
+ def error_on_none(value: T | None) -> T:
17
+ if value is None:
18
+ raise ComposeError
19
+ return value
20
+
21
+
22
+ def generate(
23
+ subnodes: tuple[type[Node], ...],
24
+ func: typing.Callable[..., typing.Any],
25
+ casts: tuple[typing.Callable, ...] = (cast_false_to_none, error_on_none),
26
+ ) -> type[ContainerNode]:
27
+ async def compose(**kw: typing.Any) -> typing.Any:
28
+ args = await ContainerNode.compose(**kw)
29
+ result = func(*args)
30
+ if inspect.isawaitable(result):
31
+ result = await result
32
+ for cast in casts:
33
+ result = cast(result)
34
+ return result
35
+
36
+ container = ContainerNode.link_nodes(list(subnodes))
37
+ return type("_ContainerNode", (container,), {"compose": compose})
38
+
39
+
40
+ __all__ = ("generate",)
@@ -0,0 +1,12 @@
1
+ from telegrinder.bot.cute_types import UpdateCute
2
+
3
+ from .base import ScalarNode
4
+
5
+
6
+ class UpdateNode(ScalarNode, UpdateCute):
7
+ @classmethod
8
+ async def compose(cls, update: UpdateCute) -> UpdateCute:
9
+ return update
10
+
11
+
12
+ __all__ = ("UpdateNode",)
telegrinder/rules.py CHANGED
@@ -1 +1 @@
1
- from .bot.rules import *
1
+ from .bot.rules import * # noqa: F403