telegrinder 0.3.4__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 (192) hide show
  1. telegrinder/__init__.py +148 -149
  2. telegrinder/api/__init__.py +9 -8
  3. telegrinder/api/api.py +101 -93
  4. telegrinder/api/error.py +20 -16
  5. telegrinder/api/response.py +20 -20
  6. telegrinder/api/token.py +36 -36
  7. telegrinder/bot/__init__.py +72 -66
  8. telegrinder/bot/bot.py +83 -76
  9. telegrinder/bot/cute_types/__init__.py +19 -17
  10. telegrinder/bot/cute_types/base.py +184 -258
  11. telegrinder/bot/cute_types/callback_query.py +400 -385
  12. telegrinder/bot/cute_types/chat_join_request.py +62 -61
  13. telegrinder/bot/cute_types/chat_member_updated.py +157 -160
  14. telegrinder/bot/cute_types/inline_query.py +44 -43
  15. telegrinder/bot/cute_types/message.py +2590 -2637
  16. telegrinder/bot/cute_types/pre_checkout_query.py +42 -0
  17. telegrinder/bot/cute_types/update.py +112 -104
  18. telegrinder/bot/cute_types/utils.py +62 -95
  19. telegrinder/bot/dispatch/__init__.py +59 -55
  20. telegrinder/bot/dispatch/abc.py +76 -77
  21. telegrinder/bot/dispatch/context.py +96 -98
  22. telegrinder/bot/dispatch/dispatch.py +254 -202
  23. telegrinder/bot/dispatch/handler/__init__.py +13 -13
  24. telegrinder/bot/dispatch/handler/abc.py +23 -24
  25. telegrinder/bot/dispatch/handler/audio_reply.py +44 -44
  26. telegrinder/bot/dispatch/handler/base.py +57 -57
  27. telegrinder/bot/dispatch/handler/document_reply.py +44 -44
  28. telegrinder/bot/dispatch/handler/func.py +129 -135
  29. telegrinder/bot/dispatch/handler/media_group_reply.py +44 -43
  30. telegrinder/bot/dispatch/handler/message_reply.py +36 -36
  31. telegrinder/bot/dispatch/handler/photo_reply.py +44 -44
  32. telegrinder/bot/dispatch/handler/sticker_reply.py +37 -37
  33. telegrinder/bot/dispatch/handler/video_reply.py +44 -44
  34. telegrinder/bot/dispatch/middleware/__init__.py +3 -3
  35. telegrinder/bot/dispatch/middleware/abc.py +97 -22
  36. telegrinder/bot/dispatch/middleware/global_middleware.py +70 -0
  37. telegrinder/bot/dispatch/process.py +151 -157
  38. telegrinder/bot/dispatch/return_manager/__init__.py +15 -13
  39. telegrinder/bot/dispatch/return_manager/abc.py +104 -108
  40. telegrinder/bot/dispatch/return_manager/callback_query.py +20 -20
  41. telegrinder/bot/dispatch/return_manager/inline_query.py +15 -15
  42. telegrinder/bot/dispatch/return_manager/message.py +36 -36
  43. telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +20 -0
  44. telegrinder/bot/dispatch/view/__init__.py +15 -13
  45. telegrinder/bot/dispatch/view/abc.py +45 -41
  46. telegrinder/bot/dispatch/view/base.py +231 -200
  47. telegrinder/bot/dispatch/view/box.py +140 -129
  48. telegrinder/bot/dispatch/view/callback_query.py +16 -17
  49. telegrinder/bot/dispatch/view/chat_join_request.py +11 -16
  50. telegrinder/bot/dispatch/view/chat_member.py +37 -39
  51. telegrinder/bot/dispatch/view/inline_query.py +16 -17
  52. telegrinder/bot/dispatch/view/message.py +43 -44
  53. telegrinder/bot/dispatch/view/pre_checkout_query.py +16 -0
  54. telegrinder/bot/dispatch/view/raw.py +116 -114
  55. telegrinder/bot/dispatch/waiter_machine/__init__.py +17 -17
  56. telegrinder/bot/dispatch/waiter_machine/actions.py +14 -13
  57. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +8 -8
  58. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +55 -55
  59. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +59 -57
  60. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +51 -51
  61. telegrinder/bot/dispatch/waiter_machine/hasher/state.py +20 -19
  62. telegrinder/bot/dispatch/waiter_machine/machine.py +251 -172
  63. telegrinder/bot/dispatch/waiter_machine/middleware.py +94 -89
  64. telegrinder/bot/dispatch/waiter_machine/short_state.py +57 -68
  65. telegrinder/bot/polling/__init__.py +4 -4
  66. telegrinder/bot/polling/abc.py +25 -25
  67. telegrinder/bot/polling/polling.py +139 -131
  68. telegrinder/bot/rules/__init__.py +85 -62
  69. telegrinder/bot/rules/abc.py +213 -206
  70. telegrinder/bot/rules/callback_data.py +122 -163
  71. telegrinder/bot/rules/chat_join.py +45 -43
  72. telegrinder/bot/rules/command.py +126 -126
  73. telegrinder/bot/rules/enum_text.py +33 -36
  74. telegrinder/bot/rules/func.py +28 -26
  75. telegrinder/bot/rules/fuzzy.py +24 -24
  76. telegrinder/bot/rules/id.py +24 -0
  77. telegrinder/bot/rules/inline.py +58 -56
  78. telegrinder/bot/rules/integer.py +21 -20
  79. telegrinder/bot/rules/is_from.py +127 -127
  80. telegrinder/bot/rules/logic.py +18 -0
  81. telegrinder/bot/rules/markup.py +42 -43
  82. telegrinder/bot/rules/mention.py +14 -14
  83. telegrinder/bot/rules/message.py +15 -17
  84. telegrinder/bot/rules/message_entities.py +33 -35
  85. telegrinder/bot/rules/node.py +33 -27
  86. telegrinder/bot/rules/payload.py +81 -0
  87. telegrinder/bot/rules/payment_invoice.py +29 -0
  88. telegrinder/bot/rules/regex.py +36 -37
  89. telegrinder/bot/rules/rule_enum.py +72 -72
  90. telegrinder/bot/rules/start.py +42 -42
  91. telegrinder/bot/rules/state.py +35 -37
  92. telegrinder/bot/rules/text.py +38 -33
  93. telegrinder/bot/rules/update.py +15 -15
  94. telegrinder/bot/scenario/__init__.py +5 -5
  95. telegrinder/bot/scenario/abc.py +17 -19
  96. telegrinder/bot/scenario/checkbox.py +174 -176
  97. telegrinder/bot/scenario/choice.py +48 -51
  98. telegrinder/client/__init__.py +12 -4
  99. telegrinder/client/abc.py +100 -75
  100. telegrinder/client/aiohttp.py +134 -130
  101. telegrinder/client/form_data.py +31 -0
  102. telegrinder/client/sonic.py +212 -0
  103. telegrinder/model.py +208 -315
  104. telegrinder/modules.py +239 -237
  105. telegrinder/msgspec_json.py +14 -14
  106. telegrinder/msgspec_utils.py +478 -410
  107. telegrinder/node/__init__.py +86 -25
  108. telegrinder/node/attachment.py +163 -87
  109. telegrinder/node/base.py +288 -160
  110. telegrinder/node/callback_query.py +54 -53
  111. telegrinder/node/command.py +34 -33
  112. telegrinder/node/composer.py +163 -198
  113. telegrinder/node/container.py +33 -27
  114. telegrinder/node/either.py +82 -0
  115. telegrinder/node/event.py +54 -65
  116. telegrinder/node/file.py +51 -0
  117. telegrinder/node/me.py +15 -16
  118. telegrinder/node/payload.py +78 -0
  119. telegrinder/node/polymorphic.py +67 -48
  120. telegrinder/node/rule.py +72 -76
  121. telegrinder/node/scope.py +36 -38
  122. telegrinder/node/source.py +87 -71
  123. telegrinder/node/text.py +53 -41
  124. telegrinder/node/tools/__init__.py +3 -3
  125. telegrinder/node/tools/generator.py +36 -40
  126. telegrinder/py.typed +0 -0
  127. telegrinder/rules.py +1 -62
  128. telegrinder/tools/__init__.py +152 -93
  129. telegrinder/tools/adapter/__init__.py +19 -0
  130. telegrinder/tools/adapter/abc.py +49 -0
  131. telegrinder/tools/adapter/dataclass.py +56 -0
  132. telegrinder/{bot/rules → tools}/adapter/errors.py +5 -5
  133. telegrinder/{bot/rules → tools}/adapter/event.py +63 -65
  134. telegrinder/{bot/rules → tools}/adapter/node.py +46 -48
  135. telegrinder/{bot/rules → tools}/adapter/raw_event.py +27 -27
  136. telegrinder/{bot/rules → tools}/adapter/raw_update.py +30 -30
  137. telegrinder/tools/buttons.py +106 -80
  138. telegrinder/tools/callback_data_serilization/__init__.py +5 -0
  139. telegrinder/tools/callback_data_serilization/abc.py +51 -0
  140. telegrinder/tools/callback_data_serilization/json_ser.py +60 -0
  141. telegrinder/tools/callback_data_serilization/msgpack_ser.py +172 -0
  142. telegrinder/tools/error_handler/__init__.py +7 -7
  143. telegrinder/tools/error_handler/abc.py +30 -33
  144. telegrinder/tools/error_handler/error.py +9 -9
  145. telegrinder/tools/error_handler/error_handler.py +179 -193
  146. telegrinder/tools/formatting/__init__.py +83 -63
  147. telegrinder/tools/formatting/deep_links.py +541 -0
  148. telegrinder/tools/formatting/{html.py → html_formatter.py} +266 -294
  149. telegrinder/tools/formatting/spec_html_formats.py +71 -117
  150. telegrinder/tools/functional.py +8 -12
  151. telegrinder/tools/global_context/__init__.py +7 -7
  152. telegrinder/tools/global_context/abc.py +63 -63
  153. telegrinder/tools/global_context/global_context.py +387 -412
  154. telegrinder/tools/global_context/telegrinder_ctx.py +27 -27
  155. telegrinder/tools/i18n/__init__.py +7 -7
  156. telegrinder/tools/i18n/abc.py +30 -30
  157. telegrinder/tools/i18n/middleware/__init__.py +3 -3
  158. telegrinder/tools/i18n/middleware/abc.py +22 -25
  159. telegrinder/tools/i18n/simple.py +43 -43
  160. telegrinder/tools/input_file_directory.py +30 -0
  161. telegrinder/tools/keyboard.py +128 -128
  162. telegrinder/tools/lifespan.py +105 -0
  163. telegrinder/tools/limited_dict.py +32 -37
  164. telegrinder/tools/loop_wrapper/__init__.py +4 -4
  165. telegrinder/tools/loop_wrapper/abc.py +20 -15
  166. telegrinder/tools/loop_wrapper/loop_wrapper.py +169 -224
  167. telegrinder/tools/magic.py +307 -157
  168. telegrinder/tools/parse_mode.py +6 -6
  169. telegrinder/tools/state_storage/__init__.py +4 -4
  170. telegrinder/tools/state_storage/abc.py +31 -35
  171. telegrinder/tools/state_storage/memory.py +25 -25
  172. telegrinder/tools/strings.py +13 -0
  173. telegrinder/types/__init__.py +268 -260
  174. telegrinder/types/enums.py +711 -701
  175. telegrinder/types/input_file.py +51 -0
  176. telegrinder/types/methods.py +5055 -4633
  177. telegrinder/types/objects.py +7058 -6950
  178. telegrinder/verification_utils.py +30 -32
  179. {telegrinder-0.3.4.dist-info → telegrinder-0.4.0.dist-info}/LICENSE +22 -22
  180. telegrinder-0.4.0.dist-info/METADATA +144 -0
  181. telegrinder-0.4.0.dist-info/RECORD +182 -0
  182. {telegrinder-0.3.4.dist-info → telegrinder-0.4.0.dist-info}/WHEEL +1 -1
  183. telegrinder/bot/rules/adapter/__init__.py +0 -17
  184. telegrinder/bot/rules/adapter/abc.py +0 -31
  185. telegrinder/node/message.py +0 -14
  186. telegrinder/node/update.py +0 -15
  187. telegrinder/tools/formatting/links.py +0 -38
  188. telegrinder/tools/kb_set/__init__.py +0 -4
  189. telegrinder/tools/kb_set/base.py +0 -15
  190. telegrinder/tools/kb_set/yaml.py +0 -63
  191. telegrinder-0.3.4.dist-info/METADATA +0 -110
  192. telegrinder-0.3.4.dist-info/RECORD +0 -165
@@ -0,0 +1,51 @@
1
+ import typing
2
+
3
+ import telegrinder.types as tg_types
4
+ from telegrinder.api.api import API
5
+ from telegrinder.node.attachment import Animation, Audio, Document, Photo, Video, VideoNote, Voice
6
+ from telegrinder.node.base import FactoryNode
7
+
8
+ type Attachment = Animation | Audio | Document | Photo | Video | VideoNote | Voice
9
+
10
+
11
+ class _FileId(FactoryNode):
12
+ attachment_node: type[Attachment]
13
+
14
+ def __class_getitem__(cls, attachment_node: type[Attachment], /):
15
+ return cls(attachment_node=attachment_node)
16
+
17
+ @classmethod
18
+ def get_subnodes(cls):
19
+ return {"attach": cls.attachment_node}
20
+
21
+ @classmethod
22
+ def compose(cls, attach: Attachment) -> str:
23
+ if isinstance(attach, Photo):
24
+ return attach.sizes[-1].file_id
25
+ return attach.file_id
26
+
27
+
28
+ class _File(FactoryNode):
29
+ attachment_node: type[Attachment]
30
+
31
+ def __class_getitem__(cls, attachment_node: type[Attachment], /):
32
+ return cls(attachment_node=attachment_node)
33
+
34
+ @classmethod
35
+ def get_subnodes(cls):
36
+ return {"file_id": _FileId[cls.attachment_node]}
37
+
38
+ @classmethod
39
+ async def compose(cls, file_id: str, api: API) -> tg_types.File:
40
+ return (await api.get_file(file_id=file_id)).expect("File can't be downloaded.")
41
+
42
+
43
+ if typing.TYPE_CHECKING:
44
+ type FileId[T: Attachment] = str
45
+ type File[T: Attachment] = tg_types.File
46
+ else:
47
+ FileId = _FileId
48
+ File = _File
49
+
50
+
51
+ __all__ = ("File", "FileId")
telegrinder/node/me.py CHANGED
@@ -1,16 +1,15 @@
1
- from telegrinder.api.api import API
2
- from telegrinder.node.base import ComposeError, ScalarNode
3
- from telegrinder.node.scope import GLOBAL
4
- from telegrinder.types.objects import User
5
-
6
-
7
- class Me(ScalarNode, User):
8
- scope = GLOBAL
9
-
10
- @classmethod
11
- async def compose(cls, api: API) -> User:
12
- me = await api.get_me()
13
- return me.expect(ComposeError("Can't complete get_me request"))
14
-
15
-
16
- __all__ = ("Me",)
1
+ from telegrinder.api.api import API
2
+ from telegrinder.node.base import ComposeError, scalar_node
3
+ from telegrinder.node.scope import GLOBAL
4
+ from telegrinder.types.objects import User
5
+
6
+
7
+ @scalar_node(scope=GLOBAL)
8
+ class Me:
9
+ @classmethod
10
+ async def compose(cls, api: API) -> User:
11
+ me = await api.get_me()
12
+ return me.expect(ComposeError("Can't complete api.get_me() request."))
13
+
14
+
15
+ __all__ = ("Me",)
@@ -0,0 +1,78 @@
1
+ import dataclasses
2
+ import typing
3
+
4
+ from fntypes.result import Error, Ok
5
+
6
+ from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
7
+ from telegrinder.bot.cute_types.message import MessageCute
8
+ from telegrinder.bot.cute_types.pre_checkout_query import PreCheckoutQueryCute
9
+ from telegrinder.node.base import ComposeError, DataNode, FactoryNode, GlobalNode, scalar_node
10
+ from telegrinder.node.polymorphic import Polymorphic, impl
11
+ from telegrinder.tools.callback_data_serilization import ABCDataSerializer, JSONSerializer
12
+
13
+
14
+ @scalar_node[str]
15
+ class Payload(Polymorphic):
16
+ @impl
17
+ def compose_pre_checkout_query(cls, event: PreCheckoutQueryCute) -> str:
18
+ return event.invoice_payload
19
+
20
+ @impl
21
+ def compose_callback_query(cls, event: CallbackQueryCute) -> str:
22
+ return event.data.expect("CallbackQuery has no data.")
23
+
24
+ @impl
25
+ def compose_message(cls, event: MessageCute) -> str:
26
+ return event.successful_payment.map(
27
+ lambda payment: payment.invoice_payload,
28
+ ).expect("Message has no successful payment.")
29
+
30
+
31
+ @dataclasses.dataclass(frozen=True, slots=True)
32
+ class PayloadSerializer[T: type[ABCDataSerializer[typing.Any]]](DataNode, GlobalNode[T]):
33
+ serializer: type[ABCDataSerializer[typing.Any]]
34
+
35
+ @classmethod
36
+ def compose(cls) -> typing.Self:
37
+ return cls(serializer=cls.get(default=JSONSerializer))
38
+
39
+
40
+ class _PayloadData(FactoryNode):
41
+ data_type: type[typing.Any]
42
+ serializer: type[ABCDataSerializer[typing.Any]] | None = None
43
+
44
+ def __class_getitem__(
45
+ cls,
46
+ data_type: type[typing.Any] | tuple[type[typing.Any], type[ABCDataSerializer[typing.Any]]],
47
+ /,
48
+ ):
49
+ data_type, serializer = (data_type, None) if not isinstance(data_type, tuple) else data_type
50
+ return cls(data_type=data_type, serializer=serializer)
51
+
52
+ @classmethod
53
+ def compose(cls, payload: Payload, payload_serializer: PayloadSerializer) -> typing.Any:
54
+ serializer = cls.serializer or payload_serializer.serializer
55
+ match serializer(cls.data_type).deserialize(payload):
56
+ case Ok(value):
57
+ return value
58
+ case Error(err):
59
+ raise ComposeError(err)
60
+
61
+
62
+ if typing.TYPE_CHECKING:
63
+ import typing_extensions
64
+
65
+ DataType = typing.TypeVar("DataType")
66
+ Serializer = typing_extensions.TypeVar(
67
+ "Serializer",
68
+ bound=ABCDataSerializer,
69
+ default=JSONSerializer[typing.Any],
70
+ )
71
+
72
+ type PayloadDataType[DataType, Serializer] = typing.Annotated[DataType, Serializer]
73
+ PayloadData: typing.TypeAlias = PayloadDataType[DataType, Serializer]
74
+ else:
75
+ PayloadData = _PayloadData
76
+
77
+
78
+ __all__ = ("Payload", "PayloadData", "PayloadSerializer")
@@ -1,48 +1,67 @@
1
- import typing
2
-
3
- from telegrinder.bot.dispatch.context import Context
4
- from telegrinder.modules import logger
5
- from telegrinder.node.base import ComposeError, Node
6
- from telegrinder.node.composer import CONTEXT_STORE_NODES_KEY, Composition, NodeSession
7
- from telegrinder.node.scope import NodeScope
8
- from telegrinder.node.update import UpdateNode
9
- from telegrinder.tools.magic import get_impls, impl
10
-
11
-
12
- class Polymorphic(Node):
13
- @classmethod
14
- async def compose(cls, update: UpdateNode, context: Context) -> typing.Any:
15
- logger.debug(f"Composing polymorphic node {cls.__name__!r}...")
16
- scope = getattr(cls, "scope", None)
17
- node_ctx = context.get_or_set(CONTEXT_STORE_NODES_KEY, {})
18
-
19
- for i, impl_ in enumerate(get_impls(cls)):
20
- logger.debug("Checking impl {!r}...", impl_.__name__)
21
- composition = Composition(impl_, True)
22
- node_collection = await composition.compose_nodes(update, context)
23
- if node_collection is None:
24
- logger.debug("Impl {!r} composition failed!", impl_.__name__)
25
- continue
26
-
27
- # To determine whether this is a right morph, all subnodes must be resolved
28
- if scope is NodeScope.PER_EVENT and (cls, i) in node_ctx:
29
- logger.debug(
30
- "Morph is already cached as per_event node, using its value. Impl {!r} succeeded!",
31
- impl_.__name__,
32
- )
33
- res: NodeSession = node_ctx[(cls, i)]
34
- await node_collection.close_all()
35
- return res.value
36
-
37
- result = await composition(cls, **node_collection.values)
38
- if scope is NodeScope.PER_EVENT:
39
- node_ctx[(cls, i)] = NodeSession(cls, result, {})
40
-
41
- await node_collection.close_all(with_value=result)
42
- logger.debug("Impl {!r} succeeded with value: {!r}", impl_.__name__, result)
43
- return result
44
-
45
- raise ComposeError("No implementation found.")
46
-
47
-
48
- __all__ = ("Polymorphic", "impl")
1
+ import inspect
2
+ import typing
3
+
4
+ from fntypes.result import Error, Ok
5
+
6
+ from telegrinder.api.api import API
7
+ from telegrinder.bot.cute_types.update import UpdateCute
8
+ from telegrinder.bot.dispatch.context import Context
9
+ from telegrinder.modules import logger
10
+ from telegrinder.node.base import ComposeError, Node, get_nodes
11
+ from telegrinder.node.composer import CONTEXT_STORE_NODES_KEY, NodeSession, compose_nodes
12
+ from telegrinder.node.scope import NodeScope
13
+ from telegrinder.tools.magic import get_impls, impl, magic_bundle
14
+ from telegrinder.types.objects import Update
15
+
16
+
17
+ class Polymorphic(Node):
18
+ @classmethod
19
+ async def compose(cls, raw_update: Update, update: UpdateCute, context: Context) -> typing.Any:
20
+ logger.debug(f"Composing polymorphic node {cls.__name__!r}...")
21
+ scope = getattr(cls, "scope", None)
22
+ node_ctx = context.get_or_set(CONTEXT_STORE_NODES_KEY, {})
23
+ data = {
24
+ API: update.ctx_api,
25
+ Context: context,
26
+ Update: raw_update,
27
+ }
28
+
29
+ for i, impl_ in enumerate(get_impls(cls)):
30
+ logger.debug("Checking impl {!r}...", impl_.__name__)
31
+ node_collection = None
32
+
33
+ match await compose_nodes(get_nodes(impl_), context, data=data):
34
+ case Ok(col):
35
+ node_collection = col
36
+ case Error(err):
37
+ logger.debug(f"Composition failed with error: {err!r}")
38
+
39
+ if node_collection is None:
40
+ logger.debug("Impl {!r} composition failed!", impl_.__name__)
41
+ continue
42
+
43
+ # To determine whether this is a right morph, all subnodes must be resolved
44
+ if scope is NodeScope.PER_EVENT and (cls, i) in node_ctx:
45
+ logger.debug(
46
+ "Morph is already cached as per_event node, using its value. Impl {!r} succeeded!",
47
+ impl_.__name__,
48
+ )
49
+ res: NodeSession = node_ctx[(cls, i)]
50
+ await node_collection.close_all()
51
+ return res.value
52
+
53
+ result = impl_(cls, **node_collection.values | magic_bundle(impl_, data, typebundle=True))
54
+ if inspect.isawaitable(result):
55
+ result = await result
56
+
57
+ if scope is NodeScope.PER_EVENT:
58
+ node_ctx[(cls, i)] = NodeSession(cls, result, {})
59
+
60
+ await node_collection.close_all(with_value=result)
61
+ logger.debug("Impl {!r} succeeded with value: {!r}", impl_.__name__, result)
62
+ return result
63
+
64
+ raise ComposeError("No implementation found.")
65
+
66
+
67
+ __all__ = ("Polymorphic", "impl")
telegrinder/node/rule.py CHANGED
@@ -1,76 +1,72 @@
1
- import dataclasses
2
- import importlib
3
- import typing
4
-
5
- from telegrinder.bot.dispatch.context import Context
6
- from telegrinder.node.base import ComposeError, Node
7
- from telegrinder.node.update import UpdateNode
8
-
9
- if typing.TYPE_CHECKING:
10
- from telegrinder.bot.dispatch.process import check_rule
11
- from telegrinder.bot.rules.abc import ABCRule
12
-
13
-
14
- class RuleChain(dict[str, typing.Any], Node):
15
- dataclass: type[typing.Any] = dict
16
- rules: tuple["ABCRule", ...] = ()
17
-
18
- def __init_subclass__(cls, *args: typing.Any, **kwargs: typing.Any) -> None:
19
- super().__init_subclass__(*args, **kwargs)
20
-
21
- if cls.__name__ == "_RuleNode":
22
- return
23
- cls.dataclass = cls.generate_node_dataclass(cls)
24
-
25
- def __new__(cls, *rules: "ABCRule") -> type[Node]:
26
- return type("_RuleNode", (cls,), {"dataclass": dict, "rules": rules}) # type: ignore
27
-
28
- def __class_getitem__(cls, items: "ABCRule | tuple[ABCRule, ...]", /) -> typing.Self:
29
- if not isinstance(items, tuple):
30
- items = (items,)
31
- return cls(*items)
32
-
33
- @staticmethod
34
- def generate_node_dataclass(cls_: type["RuleChain"]): # noqa: ANN205
35
- return dataclasses.dataclass(type(cls_.__name__, (object,), dict(cls_.__dict__)))
36
-
37
- @classmethod
38
- async def compose(cls, update: UpdateNode) -> typing.Any:
39
- # Hack to avoid circular import
40
- globalns = globals()
41
- if "check_rule" not in globalns:
42
- globalns.update(
43
- {
44
- "check_rule": getattr(
45
- importlib.import_module("telegrinder.bot.dispatch.process"),
46
- "check_rule",
47
- ),
48
- },
49
- )
50
-
51
- ctx = Context()
52
- for rule in cls.rules:
53
- if not await check_rule(update.api, rule, update, ctx):
54
- raise ComposeError(f"Rule {rule!r} failed!")
55
-
56
- try:
57
- if dataclasses.is_dataclass(cls.dataclass):
58
- return cls.dataclass(**{k: ctx[k] for k in cls.__annotations__})
59
- return cls.dataclass(**ctx)
60
- except Exception as exc:
61
- raise ComposeError(f"Dataclass validation error: {exc}")
62
-
63
- @classmethod
64
- def as_node(cls) -> type[typing.Self]:
65
- return cls
66
-
67
- @classmethod
68
- def get_subnodes(cls) -> dict[typing.Literal["update"], type[UpdateNode]]:
69
- return {"update": UpdateNode}
70
-
71
- @classmethod
72
- def is_generator(cls) -> typing.Literal[False]:
73
- return False
74
-
75
-
76
- __all__ = ("RuleChain",)
1
+ import dataclasses
2
+ import importlib
3
+ import typing
4
+
5
+ from telegrinder.bot.cute_types.update import UpdateCute
6
+ from telegrinder.bot.dispatch.context import Context
7
+ from telegrinder.node.base import ComposeError, Node
8
+
9
+ if typing.TYPE_CHECKING:
10
+ from telegrinder.bot.dispatch.process import check_rule
11
+ from telegrinder.bot.rules.abc import ABCRule
12
+
13
+
14
+ class RuleChain(dict[str, typing.Any], Node):
15
+ dataclass: type[typing.Any] = dict
16
+ rules: tuple["ABCRule", ...] = ()
17
+
18
+ def __init_subclass__(cls, *args: typing.Any, **kwargs: typing.Any) -> None:
19
+ super().__init_subclass__(*args, **kwargs)
20
+
21
+ if cls.__name__ == "_RuleNode":
22
+ return
23
+ cls.dataclass = cls.generate_node_dataclass(cls)
24
+
25
+ def __new__(cls, *rules: "ABCRule") -> type[Node]:
26
+ return type("_RuleNode", (cls,), {"dataclass": dict, "rules": rules}) # type: ignore
27
+
28
+ def __class_getitem__(cls, items: "ABCRule | tuple[ABCRule, ...]", /) -> typing.Self:
29
+ if not isinstance(items, tuple):
30
+ items = (items,)
31
+ return cls(*items)
32
+
33
+ @staticmethod
34
+ def generate_node_dataclass(cls_: type["RuleChain"]): # noqa: ANN205
35
+ return dataclasses.dataclass(type(cls_.__name__, (object,), dict(cls_.__dict__)))
36
+
37
+ @classmethod
38
+ async def compose(cls, update: UpdateCute) -> typing.Any:
39
+ # Hack to avoid circular import
40
+ globalns = globals()
41
+ if "check_rule" not in globalns:
42
+ globalns.update(
43
+ {
44
+ "check_rule": getattr(
45
+ importlib.import_module("telegrinder.bot.dispatch.process"),
46
+ "check_rule",
47
+ ),
48
+ },
49
+ )
50
+
51
+ ctx = Context()
52
+ for rule in cls.rules:
53
+ if not await check_rule(update.api, rule, update, ctx):
54
+ raise ComposeError(f"Rule {rule!r} failed!")
55
+
56
+ try:
57
+ if dataclasses.is_dataclass(cls.dataclass):
58
+ return cls.dataclass(**{k: ctx[k] for k in cls.__annotations__})
59
+ return cls.dataclass(**ctx)
60
+ except Exception as exc:
61
+ raise ComposeError(f"Dataclass validation error: {exc}")
62
+
63
+ @classmethod
64
+ def as_node(cls) -> type[typing.Self]:
65
+ return cls
66
+
67
+ @classmethod
68
+ def is_generator(cls) -> typing.Literal[False]:
69
+ return False
70
+
71
+
72
+ __all__ = ("RuleChain",)
telegrinder/node/scope.py CHANGED
@@ -1,44 +1,42 @@
1
- import enum
2
- import typing
3
-
4
- if typing.TYPE_CHECKING:
5
- from .base import Node
6
-
7
- T = typing.TypeVar("T", bound=type["Node"])
8
-
9
-
10
- class NodeScope(enum.Enum):
11
- GLOBAL = enum.auto()
12
- PER_EVENT = enum.auto()
13
- PER_CALL = enum.auto()
14
-
15
-
16
- PER_EVENT = NodeScope.PER_EVENT
17
- PER_CALL = NodeScope.PER_CALL
18
- GLOBAL = NodeScope.GLOBAL
19
-
20
-
21
- def per_call(node: T) -> T:
22
- setattr(node, "scope", PER_CALL)
23
- return node
24
-
25
-
26
- def per_event(node: T) -> T:
27
- setattr(node, "scope", PER_EVENT)
28
- return node
29
-
30
-
31
- def global_node(node: T) -> T:
32
- setattr(node, "scope", GLOBAL)
33
- return node
34
-
35
-
36
- __all__ = (
1
+ import enum
2
+ import typing
3
+
4
+ if typing.TYPE_CHECKING:
5
+ from .base import IsNode
6
+
7
+
8
+ class NodeScope(enum.Enum):
9
+ GLOBAL = enum.auto()
10
+ PER_EVENT = enum.auto()
11
+ PER_CALL = enum.auto()
12
+
13
+
14
+ PER_EVENT = NodeScope.PER_EVENT
15
+ PER_CALL = NodeScope.PER_CALL
16
+ GLOBAL = NodeScope.GLOBAL
17
+
18
+
19
+ def per_call[T: IsNode](node: type[T]) -> type[T]:
20
+ setattr(node, "scope", PER_CALL)
21
+ return node
22
+
23
+
24
+ def per_event[T: IsNode](node: type[T]) -> type[T]:
25
+ setattr(node, "scope", PER_EVENT)
26
+ return node
27
+
28
+
29
+ def global_node[T: IsNode](node: type[T]) -> type[T]:
30
+ setattr(node, "scope", GLOBAL)
31
+ return node
32
+
33
+
34
+ __all__ = (
37
35
  "GLOBAL",
38
36
  "NodeScope",
39
37
  "PER_CALL",
40
38
  "PER_EVENT",
41
39
  "global_node",
42
40
  "per_call",
43
- "per_event",
44
- )
41
+ "per_event",
42
+ )
@@ -1,71 +1,87 @@
1
- import dataclasses
2
- import typing
3
-
4
- from fntypes.option import Nothing, Option
5
-
6
- from telegrinder.api.api import API
7
- from telegrinder.bot.cute_types import ChatJoinRequestCute
8
- from telegrinder.node.base import ComposeError, DataNode, ScalarNode
9
- from telegrinder.node.callback_query import CallbackQueryNode
10
- from telegrinder.node.event import EventNode
11
- from telegrinder.node.message import MessageNode
12
- from telegrinder.node.polymorphic import Polymorphic, impl
13
- from telegrinder.types.objects import Chat, Message, User
14
-
15
-
16
- @dataclasses.dataclass(kw_only=True, slots=True)
17
- class Source(Polymorphic, DataNode):
18
- api: API
19
- chat: Chat
20
- from_user: User
21
- thread_id: Option[int] = dataclasses.field(default_factory=lambda: Nothing())
22
-
23
- @impl
24
- def compose_message(cls, message: MessageNode) -> typing.Self:
25
- return cls(
26
- api=message.ctx_api,
27
- chat=message.chat,
28
- from_user=message.from_.expect(ComposeError("MessageNode has no from_user")),
29
- thread_id=message.message_thread_id,
30
- )
31
-
32
- @impl
33
- def compose_callback_query(cls, callback_query: CallbackQueryNode) -> typing.Self:
34
- return cls(
35
- api=callback_query.ctx_api,
36
- chat=callback_query.chat.expect(ComposeError("CallbackQueryNode has no chat")),
37
- from_user=callback_query.from_user,
38
- thread_id=callback_query.message_thread_id,
39
- )
40
-
41
- @impl
42
- def compose_chat_join_request(cls, chat_join_request: EventNode[ChatJoinRequestCute]) -> typing.Self:
43
- return cls(
44
- api=chat_join_request.ctx_api,
45
- chat=chat_join_request.chat,
46
- from_user=chat_join_request.from_user,
47
- thread_id=Nothing(),
48
- )
49
-
50
- async def send(self, text: str) -> Message:
51
- result = await self.api.send_message(
52
- chat_id=self.chat.id,
53
- message_thread_id=self.thread_id.unwrap_or_none(),
54
- text=text,
55
- )
56
- return result.unwrap()
57
-
58
-
59
- class ChatSource(ScalarNode, Chat):
60
- @classmethod
61
- def compose(cls, source: Source) -> Chat:
62
- return source.chat
63
-
64
-
65
- class UserSource(ScalarNode, User):
66
- @classmethod
67
- def compose(cls, source: Source) -> User:
68
- return source.from_user
69
-
70
-
71
- __all__ = ("ChatSource", "Source", "UserSource")
1
+ import dataclasses
2
+ import typing
3
+
4
+ from fntypes.option import Nothing, Option, Some
5
+
6
+ from telegrinder.api.api import API
7
+ from telegrinder.bot.cute_types import CallbackQueryCute, ChatJoinRequestCute, MessageCute, PreCheckoutQueryCute
8
+ from telegrinder.node.base import ComposeError, DataNode, scalar_node
9
+ from telegrinder.node.polymorphic import Polymorphic, impl
10
+ from telegrinder.types.objects import Chat, Message, User
11
+
12
+
13
+ @dataclasses.dataclass(kw_only=True, slots=True)
14
+ class Source(Polymorphic, DataNode):
15
+ api: API
16
+ from_user: User
17
+ chat: Option[Chat] = dataclasses.field(default_factory=Nothing)
18
+ thread_id: Option[int] = dataclasses.field(default_factory=Nothing)
19
+
20
+ @impl
21
+ def compose_message(cls, message: MessageCute) -> typing.Self:
22
+ return cls(
23
+ api=message.ctx_api,
24
+ from_user=message.from_user,
25
+ chat=Some(message.chat),
26
+ thread_id=message.message_thread_id,
27
+ )
28
+
29
+ @impl
30
+ def compose_callback_query(cls, callback_query: CallbackQueryCute) -> typing.Self:
31
+ return cls(
32
+ api=callback_query.ctx_api,
33
+ from_user=callback_query.from_user,
34
+ chat=callback_query.chat,
35
+ thread_id=callback_query.message_thread_id,
36
+ )
37
+
38
+ @impl
39
+ def compose_chat_join_request(cls, chat_join_request: ChatJoinRequestCute) -> typing.Self:
40
+ return cls(
41
+ api=chat_join_request.ctx_api,
42
+ from_user=chat_join_request.from_user,
43
+ chat=Some(chat_join_request.chat),
44
+ thread_id=Nothing(),
45
+ )
46
+
47
+ @impl
48
+ def compose_pre_checkout_query(cls, pre_checkout_query: PreCheckoutQueryCute) -> typing.Self:
49
+ return cls(
50
+ api=pre_checkout_query.ctx_api,
51
+ from_user=pre_checkout_query.from_user,
52
+ chat=Nothing(),
53
+ thread_id=Nothing(),
54
+ )
55
+
56
+ async def send(self, text: str, **kwargs: typing.Any) -> Message:
57
+ result = await self.api.send_message(
58
+ chat_id=self.chat.map_or(self.from_user.id, lambda chat: chat.id).unwrap(),
59
+ message_thread_id=self.thread_id.unwrap_or_none(),
60
+ text=text,
61
+ **kwargs,
62
+ )
63
+ return result.unwrap()
64
+
65
+
66
+ @scalar_node
67
+ class ChatSource:
68
+ @classmethod
69
+ def compose(cls, source: Source) -> Chat:
70
+ return source.chat.expect(ComposeError("Source has no chat."))
71
+
72
+
73
+ @scalar_node
74
+ class UserSource:
75
+ @classmethod
76
+ def compose(cls, source: Source) -> User:
77
+ return source.from_user
78
+
79
+
80
+ @scalar_node
81
+ class UserId:
82
+ @classmethod
83
+ def compose(cls, user: UserSource) -> int:
84
+ return user.id
85
+
86
+
87
+ __all__ = ("ChatSource", "Source", "UserId", "UserSource")