telegrinder 0.1.dev167__py3-none-any.whl → 0.1.dev169__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 +9 -3
  2. telegrinder/bot/__init__.py +7 -5
  3. telegrinder/bot/cute_types/base.py +12 -14
  4. telegrinder/bot/cute_types/callback_query.py +55 -44
  5. telegrinder/bot/cute_types/chat_join_request.py +8 -7
  6. telegrinder/bot/cute_types/chat_member_updated.py +23 -17
  7. telegrinder/bot/cute_types/inline_query.py +1 -1
  8. telegrinder/bot/cute_types/message.py +331 -183
  9. telegrinder/bot/cute_types/update.py +4 -8
  10. telegrinder/bot/cute_types/utils.py +1 -5
  11. telegrinder/bot/dispatch/__init__.py +2 -3
  12. telegrinder/bot/dispatch/abc.py +4 -0
  13. telegrinder/bot/dispatch/context.py +9 -4
  14. telegrinder/bot/dispatch/dispatch.py +33 -30
  15. telegrinder/bot/dispatch/handler/func.py +33 -12
  16. telegrinder/bot/dispatch/handler/message_reply.py +6 -3
  17. telegrinder/bot/dispatch/middleware/abc.py +4 -4
  18. telegrinder/bot/dispatch/process.py +40 -13
  19. telegrinder/bot/dispatch/return_manager/abc.py +12 -12
  20. telegrinder/bot/dispatch/return_manager/callback_query.py +1 -3
  21. telegrinder/bot/dispatch/return_manager/inline_query.py +1 -3
  22. telegrinder/bot/dispatch/view/abc.py +74 -31
  23. telegrinder/bot/dispatch/view/box.py +66 -50
  24. telegrinder/bot/dispatch/view/message.py +1 -5
  25. telegrinder/bot/dispatch/view/raw.py +6 -6
  26. telegrinder/bot/dispatch/waiter_machine/__init__.py +2 -1
  27. telegrinder/bot/dispatch/waiter_machine/machine.py +86 -50
  28. telegrinder/bot/dispatch/waiter_machine/middleware.py +31 -7
  29. telegrinder/bot/dispatch/waiter_machine/short_state.py +26 -7
  30. telegrinder/bot/polling/polling.py +4 -4
  31. telegrinder/bot/rules/__init__.py +9 -6
  32. telegrinder/bot/rules/abc.py +99 -22
  33. telegrinder/bot/rules/adapter/__init__.py +4 -1
  34. telegrinder/bot/rules/adapter/abc.py +11 -6
  35. telegrinder/bot/rules/adapter/errors.py +1 -2
  36. telegrinder/bot/rules/adapter/event.py +14 -9
  37. telegrinder/bot/rules/adapter/node.py +42 -0
  38. telegrinder/bot/rules/callback_data.py +13 -15
  39. telegrinder/bot/rules/chat_join.py +3 -2
  40. telegrinder/bot/rules/command.py +26 -14
  41. telegrinder/bot/rules/enum_text.py +5 -5
  42. telegrinder/bot/rules/func.py +6 -6
  43. telegrinder/bot/rules/fuzzy.py +5 -7
  44. telegrinder/bot/rules/inline.py +4 -5
  45. telegrinder/bot/rules/integer.py +10 -8
  46. telegrinder/bot/rules/is_from.py +63 -91
  47. telegrinder/bot/rules/markup.py +5 -5
  48. telegrinder/bot/rules/mention.py +4 -4
  49. telegrinder/bot/rules/message.py +1 -1
  50. telegrinder/bot/rules/node.py +27 -0
  51. telegrinder/bot/rules/regex.py +5 -5
  52. telegrinder/bot/rules/rule_enum.py +4 -4
  53. telegrinder/bot/rules/start.py +5 -5
  54. telegrinder/bot/rules/text.py +9 -13
  55. telegrinder/bot/rules/update.py +4 -4
  56. telegrinder/bot/scenario/__init__.py +3 -3
  57. telegrinder/bot/scenario/checkbox.py +5 -5
  58. telegrinder/bot/scenario/choice.py +5 -5
  59. telegrinder/model.py +49 -15
  60. telegrinder/modules.py +14 -6
  61. telegrinder/msgspec_utils.py +8 -17
  62. telegrinder/node/__init__.py +26 -8
  63. telegrinder/node/attachment.py +13 -9
  64. telegrinder/node/base.py +27 -14
  65. telegrinder/node/callback_query.py +18 -0
  66. telegrinder/node/command.py +29 -0
  67. telegrinder/node/composer.py +119 -30
  68. telegrinder/node/me.py +14 -0
  69. telegrinder/node/message.py +2 -4
  70. telegrinder/node/polymorphic.py +44 -0
  71. telegrinder/node/rule.py +26 -22
  72. telegrinder/node/scope.py +36 -0
  73. telegrinder/node/source.py +37 -10
  74. telegrinder/node/text.py +11 -5
  75. telegrinder/node/tools/__init__.py +2 -2
  76. telegrinder/node/tools/generator.py +6 -6
  77. telegrinder/tools/__init__.py +9 -14
  78. telegrinder/tools/buttons.py +23 -17
  79. telegrinder/tools/error_handler/error_handler.py +11 -14
  80. telegrinder/tools/formatting/__init__.py +0 -6
  81. telegrinder/tools/formatting/html.py +10 -12
  82. telegrinder/tools/formatting/links.py +0 -5
  83. telegrinder/tools/formatting/spec_html_formats.py +0 -11
  84. telegrinder/tools/global_context/abc.py +1 -3
  85. telegrinder/tools/global_context/global_context.py +6 -16
  86. telegrinder/tools/i18n/simple.py +1 -3
  87. telegrinder/tools/kb_set/yaml.py +1 -2
  88. telegrinder/tools/keyboard.py +7 -8
  89. telegrinder/tools/limited_dict.py +13 -3
  90. telegrinder/tools/loop_wrapper/loop_wrapper.py +6 -5
  91. telegrinder/tools/magic.py +27 -5
  92. telegrinder/types/__init__.py +20 -0
  93. telegrinder/types/enums.py +37 -31
  94. telegrinder/types/methods.py +613 -401
  95. telegrinder/types/objects.py +1151 -757
  96. {telegrinder-0.1.dev167.dist-info → telegrinder-0.1.dev169.dist-info}/LICENSE +1 -1
  97. {telegrinder-0.1.dev167.dist-info → telegrinder-0.1.dev169.dist-info}/METADATA +9 -8
  98. telegrinder-0.1.dev169.dist-info/RECORD +143 -0
  99. telegrinder/bot/dispatch/composition.py +0 -88
  100. telegrinder-0.1.dev167.dist-info/RECORD +0 -137
  101. {telegrinder-0.1.dev167.dist-info → telegrinder-0.1.dev169.dist-info}/WHEEL +0 -0
@@ -1,28 +1,24 @@
1
+ from telegrinder import node
1
2
  from telegrinder.bot.dispatch.context import Context
2
3
  from telegrinder.tools.i18n.base import ABCTranslator
3
4
 
4
- from .abc import ABC, Message, with_caching_translations
5
- from .message import MessageRule
5
+ from .abc import ABCRule, with_caching_translations
6
+ from .node import NodeRule
6
7
 
7
8
 
8
- class HasText(MessageRule):
9
- async def check(self, message: Message, ctx: Context) -> bool:
10
- return bool(message.text)
9
+ class HasText(NodeRule):
10
+ def __init__(self) -> None:
11
+ super().__init__(node.text.Text)
11
12
 
12
13
 
13
- class TextMessageRule(MessageRule, ABC, requires=[HasText()]):
14
- pass
15
-
16
-
17
- class Text(TextMessageRule):
14
+ class Text(ABCRule):
18
15
  def __init__(self, texts: str | list[str], *, ignore_case: bool = False) -> None:
19
16
  if not isinstance(texts, list):
20
17
  texts = [texts]
21
18
  self.texts = texts if not ignore_case else list(map(str.lower, texts))
22
19
  self.ignore_case = ignore_case
23
20
 
24
- async def check(self, message: Message, ctx: Context) -> bool:
25
- text = message.text.unwrap()
21
+ async def check(self, text: node.text.Text, ctx: Context) -> bool:
26
22
  return (text if not self.ignore_case else text.lower()) in self.texts
27
23
 
28
24
  @with_caching_translations
@@ -33,4 +29,4 @@ class Text(TextMessageRule):
33
29
  )
34
30
 
35
31
 
36
- __all__ = ("HasText", "Text", "TextMessageRule")
32
+ __all__ = ("HasText", "Text")
@@ -2,15 +2,15 @@ from telegrinder.bot.cute_types.update import UpdateCute
2
2
  from telegrinder.bot.dispatch.context import Context
3
3
  from telegrinder.types.enums import UpdateType
4
4
 
5
- from .abc import ABCRule, T
5
+ from .abc import ABCRule
6
6
 
7
7
 
8
- class IsUpdate(ABCRule[T]):
8
+ class IsUpdateType(ABCRule):
9
9
  def __init__(self, update_type: UpdateType, /) -> None:
10
10
  self.update_type = update_type
11
11
 
12
12
  async def check(self, event: UpdateCute, ctx: Context) -> bool:
13
- return event.update_type.unwrap_or_none() == self.update_type
13
+ return event.update_type == self.update_type
14
14
 
15
15
 
16
- __all__ = ("IsUpdate",)
16
+ __all__ = ("IsUpdateType",)
@@ -1,5 +1,5 @@
1
1
  from .abc import ABCScenario
2
- from .checkbox import Checkbox, Choice
3
- from .choice import SingleChoice
2
+ from .checkbox import Checkbox
3
+ from .choice import Choice
4
4
 
5
- __all__ = ("ABCScenario", "Checkbox", "Choice", "SingleChoice")
5
+ __all__ = ("ABCScenario", "Checkbox", "Choice")
@@ -33,13 +33,13 @@ class Checkbox(ABCScenario[CallbackQueryCute]):
33
33
  self,
34
34
  waiter_machine: WaiterMachine,
35
35
  chat_id: int,
36
- msg: str,
36
+ message: str,
37
37
  *,
38
38
  ready_text: str = "Ready",
39
39
  max_in_row: int = 3,
40
40
  ) -> None:
41
41
  self.chat_id = chat_id
42
- self.msg = msg
42
+ self.message = message
43
43
  self.choices: list[Choice] = []
44
44
  self.ready = ready_text
45
45
  self.max_in_row = max_in_row
@@ -58,7 +58,7 @@ class Checkbox(ABCScenario[CallbackQueryCute]):
58
58
  self.waiter_machine,
59
59
  self.ready,
60
60
  self.chat_id,
61
- self.msg,
61
+ self.message,
62
62
  )
63
63
 
64
64
  def get_markup(self) -> InlineKeyboardMarkup:
@@ -101,7 +101,7 @@ class Checkbox(ABCScenario[CallbackQueryCute]):
101
101
  # Toggle choice
102
102
  self.choices[i].is_picked = not self.choices[i].is_picked
103
103
  await cb.edit_text(
104
- text=self.msg,
104
+ text=self.message,
105
105
  parse_mode=self.PARSE_MODE,
106
106
  reply_markup=self.get_markup(),
107
107
  )
@@ -118,7 +118,7 @@ class Checkbox(ABCScenario[CallbackQueryCute]):
118
118
  message = (
119
119
  await api.send_message(
120
120
  chat_id=self.chat_id,
121
- text=self.msg,
121
+ text=self.message,
122
122
  parse_mode=self.PARSE_MODE,
123
123
  reply_markup=self.get_markup(),
124
124
  )
@@ -9,7 +9,7 @@ if typing.TYPE_CHECKING:
9
9
  from telegrinder.bot.dispatch.view.abc import BaseStateView
10
10
 
11
11
 
12
- class SingleChoice(Checkbox):
12
+ class Choice(Checkbox):
13
13
  async def handle(self, cb: CallbackQueryCute) -> bool:
14
14
  code = cb.data.unwrap().replace(self.random_code + "/", "", 1)
15
15
  if code == "ready":
@@ -22,9 +22,9 @@ class SingleChoice(Checkbox):
22
22
  if choice.code == code:
23
23
  self.choices[i].is_picked = True
24
24
  await cb.ctx_api.edit_message_text(
25
+ text=self.message,
25
26
  chat_id=cb.message.unwrap().v.chat.id,
26
27
  message_id=cb.message.unwrap().v.message_id,
27
- text=self.msg,
28
28
  parse_mode=self.PARSE_MODE,
29
29
  reply_markup=self.get_markup(),
30
30
  )
@@ -36,10 +36,10 @@ class SingleChoice(Checkbox):
36
36
  api: "API",
37
37
  view: "BaseStateView[CallbackQueryCute]",
38
38
  ) -> tuple[str, int]:
39
- if len([choice for choice in self.choices if choice.is_picked]) != 1:
39
+ if len(tuple(choice for choice in self.choices if choice.is_picked)) != 1:
40
40
  raise ValueError("Exactly one choice must be picked")
41
41
  choices, m_id = await super().wait(api, view)
42
- return list(choices.keys())[list(choices.values()).index(True)], m_id
42
+ return tuple(choices.keys())[tuple(choices.values()).index(True)], m_id
43
43
 
44
44
 
45
- __all__ = ("SingleChoice",)
45
+ __all__ = ("Choice",)
telegrinder/model.py CHANGED
@@ -45,14 +45,17 @@ def full_result(
45
45
 
46
46
 
47
47
  def get_params(params: dict[str, typing.Any]) -> dict[str, typing.Any]:
48
- return {
49
- k: v.unwrap() if isinstance(v, Some) else v
50
- for k, v in (
51
- *params.pop("other", {}).items(),
52
- *params.items(),
53
- )
54
- if k != "self" and type(v) not in (NoneType, Nothing)
55
- }
48
+ validated_params = {}
49
+ for k, v in (
50
+ *params.pop("other", {}).items(),
51
+ *params.items(),
52
+ ):
53
+ if isinstance(v, Proxy):
54
+ v = v.get()
55
+ if k == "self" or type(v) in (NoneType, Nothing):
56
+ continue
57
+ validated_params[k] = v.unwrap() if isinstance(v, Some) else v
58
+ return validated_params
56
59
 
57
60
 
58
61
  class Model(msgspec.Struct, **MODEL_CONFIG):
@@ -69,9 +72,7 @@ class Model(msgspec.Struct, **MODEL_CONFIG):
69
72
  if "model_as_dict" not in self.__dict__:
70
73
  self.__dict__["model_as_dict"] = msgspec.structs.asdict(self)
71
74
  return {
72
- key: value
73
- for key, value in self.__dict__["model_as_dict"].items()
74
- if key not in exclude_fields
75
+ key: value for key, value in self.__dict__["model_as_dict"].items() if key not in exclude_fields
75
76
  }
76
77
 
77
78
 
@@ -82,7 +83,7 @@ class DataConverter:
82
83
  def __repr__(self) -> str:
83
84
  return "<{}: {}>".format(
84
85
  self.__class__.__name__,
85
- ", ".join(f"{k}={v!r}" for k, v in self.converters),
86
+ ", ".join(f"{k}={v.__name__!r}" for k, v in self.converters.items()),
86
87
  )
87
88
 
88
89
  @property
@@ -129,9 +130,7 @@ class DataConverter:
129
130
  serialize: bool = True,
130
131
  ) -> dict[str, typing.Any]:
131
132
  return {
132
- k: self(v, serialize=serialize)
133
- for k, v in data.items()
134
- if type(v) not in (NoneType, Nothing)
133
+ k: self(v, serialize=serialize) for k, v in data.items() if type(v) not in (NoneType, Nothing)
135
134
  }
136
135
 
137
136
  def convert_lst(
@@ -159,8 +158,43 @@ class DataConverter:
159
158
  return data
160
159
 
161
160
 
161
+ class Proxy:
162
+ def __init__(self, cfg: "_ProxiedDict", key: str):
163
+ self.key = key
164
+ self.cfg = cfg
165
+
166
+ def get(self) -> typing.Any | None:
167
+ return self.cfg._defaults.get(self.key)
168
+
169
+
170
+ class _ProxiedDict(typing.Generic[T]):
171
+ def __init__(self, tp: type[T]) -> None:
172
+ self.type = tp
173
+ self._defaults = {}
174
+
175
+ def __setattribute__(self, name: str, value: typing.Any, /) -> None:
176
+ self._defaults[name] = value
177
+
178
+ def __getitem__(self, key: str, /) -> None:
179
+ return Proxy(self, key) # type: ignore
180
+
181
+ def __setitem__(self, key: str, value: typing.Any, /) -> None:
182
+ self._defaults[key] = value
183
+
184
+
185
+ if typing.TYPE_CHECKING:
186
+
187
+ def ProxiedDict(typed_dct: type[T]) -> T | _ProxiedDict[T]: # noqa: N802
188
+ ...
189
+
190
+ else:
191
+ ProxiedDict = _ProxiedDict
192
+
193
+
162
194
  __all__ = (
195
+ "Proxy",
163
196
  "DataConverter",
197
+ "ProxiedDict",
164
198
  "MODEL_CONFIG",
165
199
  "Model",
166
200
  "full_result",
telegrinder/modules.py CHANGED
@@ -40,10 +40,13 @@ class LoggerModule(typing.Protocol):
40
40
 
41
41
  logger: LoggerModule
42
42
  json: JSONModule = choice_in_order(
43
- ["orjson", "ujson", "hyperjson"], do_import=True, default="telegrinder.msgspec_json"
43
+ ["orjson", "ujson", "hyperjson"],
44
+ default="telegrinder.msgspec_json",
45
+ do_import=True,
44
46
  )
45
- logging_module = choice_in_order(["loguru"], default="logging")
46
47
  logging_level = os.getenv("LOGGER_LEVEL", default="DEBUG").upper()
48
+ logging_module = choice_in_order(["loguru"], default="logging")
49
+ asyncio_module = choice_in_order(["uvloop"], default="asyncio")
47
50
 
48
51
  if logging_module == "loguru":
49
52
  import os
@@ -201,9 +204,7 @@ elif logging_module == "logging":
201
204
  kwargs: dict[str, object],
202
205
  ) -> tuple[LogMessage | object, tuple[object, ...], dict[str, object]]:
203
206
  log_kwargs = {
204
- key: kwargs[key]
205
- for key in inspect.getfullargspec(self.logger._log).args[1:]
206
- if key in kwargs
207
+ key: kwargs[key] for key in inspect.getfullargspec(self.logger._log).args[1:] if key in kwargs
207
208
  }
208
209
 
209
210
  if isinstance(msg, str):
@@ -214,10 +215,17 @@ elif logging_module == "logging":
214
215
  handler = logging.StreamHandler(sys.stderr)
215
216
  handler.setFormatter(TelegrinderLoggingFormatter())
216
217
  logger = logging.getLogger("telegrinder") # type: ignore
217
- logger.setLevel(logging.getLevelName(logging_level)) # type: ignore
218
+ logger.setLevel(logging_level) # type: ignore
218
219
  logger.addHandler(handler) # type: ignore
219
220
  logger = TelegrinderLoggingStyleAdapter(logger) # type: ignore
220
221
 
222
+ if asyncio_module == "uvloop":
223
+ import asyncio
224
+
225
+ import uvloop # type: ignore
226
+
227
+ asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) # type: ignore
228
+
221
229
 
222
230
  def _set_logger_level(level):
223
231
  level = level.upper()
@@ -24,9 +24,10 @@ else:
24
24
 
25
25
 
26
26
  T = typing.TypeVar("T")
27
+ Type = typing.TypeVar("Type", bound=type | typing.Any)
27
28
  Ts = typing.TypeVarTuple("Ts")
28
29
 
29
- DecHook: typing.TypeAlias = typing.Callable[[type[T], typing.Any], object]
30
+ DecHook: typing.TypeAlias = typing.Callable[[type[T], typing.Any], typing.Any]
30
31
  EncHook: typing.TypeAlias = typing.Callable[[T], typing.Any]
31
32
 
32
33
  Nothing: typing.Final[fntypes.option.Nothing] = fntypes.option.Nothing()
@@ -67,9 +68,7 @@ def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
67
68
  reverse = False
68
69
 
69
70
  if len(set(struct_fields_match_sums.values())) != len(struct_fields_match_sums.values()):
70
- struct_fields_match_sums = {
71
- m: len(m.__struct_fields__) for m in struct_fields_match_sums
72
- }
71
+ struct_fields_match_sums = {m: len(m.__struct_fields__) for m in struct_fields_match_sums}
73
72
  reverse = True
74
73
 
75
74
  union_types = (
@@ -144,8 +143,7 @@ class Decoder:
144
143
  origin_type = t if isinstance((t := get_origin(tp)), type) else type(t)
145
144
  if origin_type not in self.dec_hooks:
146
145
  raise TypeError(
147
- f"Unknown type `{repr_type(origin_type)}`. "
148
- "You can implement decode hook for this type."
146
+ f"Unknown type `{repr_type(origin_type)}`. " "You can implement decode hook for this type."
149
147
  )
150
148
  return self.dec_hooks[origin_type](tp, obj)
151
149
 
@@ -173,7 +171,7 @@ class Decoder:
173
171
  def decode(self, buf: str | bytes) -> typing.Any: ...
174
172
 
175
173
  @typing.overload
176
- def decode(self, buf: str | bytes, *, strict: bool = True) -> typing.Any: ...
174
+ def decode(self, buf: str | bytes, *, type: type[T]) -> T: ...
177
175
 
178
176
  @typing.overload
179
177
  def decode(
@@ -184,16 +182,10 @@ class Decoder:
184
182
  strict: bool = True,
185
183
  ) -> T: ...
186
184
 
187
- def decode(
188
- self,
189
- buf: str | bytes,
190
- *,
191
- type: type[T] = typing.Any, # type: ignore
192
- strict: bool = True,
193
- ) -> T:
185
+ def decode(self, buf, *, type=object, strict=True):
194
186
  return msgspec.json.decode(
195
187
  buf,
196
- type=type,
188
+ type=typing.Any if type is object else type,
197
189
  strict=strict,
198
190
  dec_hook=self.dec_hook,
199
191
  )
@@ -240,8 +232,7 @@ class Encoder:
240
232
  origin_type = get_origin(obj.__class__)
241
233
  if origin_type not in self.enc_hooks:
242
234
  raise NotImplementedError(
243
- "Not implemented encode hook for "
244
- f"object of type `{repr_type(origin_type)}`."
235
+ "Not implemented encode hook for " f"object of type `{repr_type(origin_type)}`."
245
236
  )
246
237
  return self.enc_hooks[origin_type](obj)
247
238
 
@@ -1,17 +1,21 @@
1
1
  from .attachment import Attachment, Audio, Photo, Video
2
- from .base import ComposeError, DataNode, Node, ScalarNode
3
- from .composer import NodeCollection, NodeSession, compose_node
2
+ from .base import ComposeError, DataNode, Node, ScalarNode, is_node
3
+ from .command import CommandInfo
4
+ from .composer import Composition, NodeCollection, NodeSession, compose_node, compose_nodes
4
5
  from .container import ContainerNode
6
+ from .me import Me
5
7
  from .message import MessageNode
6
- from .rule import RuleContext
7
- from .source import Source
8
- from .text import Text
9
- from .tools import generate
8
+ from .rule import RuleChain
9
+ from .scope import GLOBAL, PER_CALL, PER_EVENT, NodeScope, global_node, per_call, per_event
10
+ from .source import ChatSource, Source, UserSource
11
+ from .text import Text, TextInteger
12
+ from .tools import generate_node
10
13
  from .update import UpdateNode
11
14
 
12
15
  __all__ = (
13
16
  "Attachment",
14
17
  "Audio",
18
+ "ChatSource",
15
19
  "ComposeError",
16
20
  "ContainerNode",
17
21
  "DataNode",
@@ -20,12 +24,26 @@ __all__ = (
20
24
  "NodeCollection",
21
25
  "NodeSession",
22
26
  "Photo",
23
- "RuleContext",
27
+ "RuleChain",
24
28
  "ScalarNode",
25
29
  "Source",
26
30
  "Text",
31
+ "TextInteger",
32
+ "UserSource",
27
33
  "UpdateNode",
28
34
  "Video",
29
35
  "compose_node",
30
- "generate",
36
+ "generate_node",
37
+ "Composition",
38
+ "is_node",
39
+ "compose_nodes",
40
+ "NodeScope",
41
+ "PER_CALL",
42
+ "PER_EVENT",
43
+ "per_call",
44
+ "per_event",
45
+ "CommandInfo",
46
+ "GLOBAL",
47
+ "global_node",
48
+ "Me",
31
49
  )
@@ -1,10 +1,10 @@
1
1
  import dataclasses
2
2
  import typing
3
3
 
4
+ from fntypes import Option, Some
4
5
  from fntypes.option import Nothing
5
6
 
6
7
  import telegrinder.types
7
- from telegrinder.msgspec_utils import Option
8
8
 
9
9
  from .base import ComposeError, DataNode, ScalarNode
10
10
  from .message import MessageNode
@@ -13,22 +13,26 @@ from .message import MessageNode
13
13
  @dataclasses.dataclass
14
14
  class Attachment(DataNode):
15
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())
16
+ audio: Option[telegrinder.types.Audio] = dataclasses.field(
17
+ default_factory=lambda: Nothing(), kw_only=True
18
+ )
18
19
  document: Option[telegrinder.types.Document] = dataclasses.field(
19
- default_factory=lambda: Nothing()
20
+ default_factory=lambda: Nothing(), kw_only=True
20
21
  )
21
22
  photo: Option[list[telegrinder.types.PhotoSize]] = dataclasses.field(
22
- default_factory=lambda: Nothing()
23
+ default_factory=lambda: Nothing(), kw_only=True
24
+ )
25
+ poll: Option[telegrinder.types.Poll] = dataclasses.field(default_factory=lambda: Nothing(), kw_only=True)
26
+ video: Option[telegrinder.types.Video] = dataclasses.field(
27
+ default_factory=lambda: Nothing(), kw_only=True
23
28
  )
24
- poll: Option[telegrinder.types.Poll] = dataclasses.field(default_factory=lambda: Nothing())
25
- video: Option[telegrinder.types.Video] = dataclasses.field(default_factory=lambda: Nothing())
26
29
 
27
30
  @classmethod
28
31
  async def compose(cls, message: MessageNode) -> "Attachment":
29
32
  for attachment_type in ("audio", "document", "photo", "poll", "video"):
30
- if (attachment := getattr(message, attachment_type, None)) is not None:
31
- return cls(attachment_type, **{attachment_type: attachment})
33
+ match getattr(message, attachment_type, Nothing()):
34
+ case Some(attachment):
35
+ return cls(attachment_type, **{attachment_type: Some(attachment)})
32
36
  return cls.compose_error("No attachment found in message")
33
37
 
34
38
 
telegrinder/node/base.py CHANGED
@@ -2,8 +2,14 @@ import abc
2
2
  import inspect
3
3
  import typing
4
4
 
5
+ from telegrinder.tools.magic import get_annotations
6
+
7
+ from .scope import NodeScope
8
+
5
9
  ComposeResult: typing.TypeAlias = (
6
- typing.Coroutine[typing.Any, typing.Any, typing.Any] | typing.AsyncGenerator[typing.Any, None]
10
+ typing.Coroutine[typing.Any, typing.Any, typing.Any]
11
+ | typing.AsyncGenerator[typing.Any, None]
12
+ | typing.Any
7
13
  )
8
14
 
9
15
 
@@ -12,10 +18,11 @@ class ComposeError(BaseException): ...
12
18
 
13
19
  class Node(abc.ABC):
14
20
  node: str = "node"
21
+ scope: NodeScope = NodeScope.PER_EVENT
15
22
 
16
23
  @classmethod
17
24
  @abc.abstractmethod
18
- def compose(cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any) -> ComposeResult:
25
+ def compose(cls, *args, **kwargs) -> ComposeResult:
19
26
  pass
20
27
 
21
28
  @classmethod
@@ -24,15 +31,7 @@ class Node(abc.ABC):
24
31
 
25
32
  @classmethod
26
33
  def get_sub_nodes(cls) -> dict[str, type[typing.Self]]:
27
- parameters = inspect.signature(cls.compose).parameters
28
-
29
- sub_nodes = {}
30
- for name, param in parameters.items():
31
- if param.annotation is inspect._empty:
32
- continue
33
- node = param.annotation
34
- sub_nodes[name] = node
35
- return sub_nodes
34
+ return get_annotations(cls.compose)
36
35
 
37
36
  @classmethod
38
37
  def as_node(cls) -> type[typing.Self]:
@@ -49,14 +48,14 @@ class DataNode(Node, abc.ABC):
49
48
  @typing.dataclass_transform()
50
49
  @classmethod
51
50
  @abc.abstractmethod
52
- async def compose(cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any) -> ComposeResult:
51
+ async def compose(cls, *args, **kwargs) -> ComposeResult:
53
52
  pass
54
53
 
55
54
 
56
55
  class ScalarNodeProto(Node, abc.ABC):
57
56
  @classmethod
58
57
  @abc.abstractmethod
59
- async def compose(cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any) -> ComposeResult:
58
+ async def compose(cls, *args, **kwargs) -> ComposeResult:
60
59
  pass
61
60
 
62
61
 
@@ -78,17 +77,31 @@ else:
78
77
  return type(
79
78
  "Scalar",
80
79
  (SCALAR_NODE,),
81
- {"as_node": classmethod(lambda cls: create_node(cls, bases, dct))},
80
+ {
81
+ "as_node": classmethod(lambda cls: create_node(cls, bases, dct)),
82
+ "scope": Node.scope,
83
+ },
82
84
  )
83
85
 
84
86
  class ScalarNode(ScalarNodeProto, abc.ABC, metaclass=create_class):
85
87
  pass
86
88
 
87
89
 
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
+
88
100
  __all__ = (
89
101
  "ComposeError",
90
102
  "DataNode",
91
103
  "Node",
92
104
  "SCALAR_NODE",
93
105
  "ScalarNode",
106
+ "is_node",
94
107
  )
@@ -0,0 +1,18 @@
1
+ from telegrinder.bot.cute_types import CallbackQueryCute
2
+
3
+ from .base import ComposeError, ScalarNode
4
+ from .update import UpdateNode
5
+
6
+
7
+ class CallbackQueryNode(ScalarNode, CallbackQueryCute):
8
+ @classmethod
9
+ async def compose(cls, update: UpdateNode) -> CallbackQueryCute:
10
+ if not update.callback_query:
11
+ raise ComposeError
12
+ return CallbackQueryCute(
13
+ **update.callback_query.unwrap().to_dict(),
14
+ api=update.api,
15
+ )
16
+
17
+
18
+ __all__ = ("CallbackQueryNode",)
@@ -0,0 +1,29 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ from fntypes import Nothing, Option, Some
4
+
5
+ from .base import DataNode
6
+ from .text import Text
7
+
8
+
9
+ def single_split(s: str, separator: str) -> tuple[str, str]:
10
+ left, *right = s.split(separator, 1)
11
+ return left, (right[0] if right else "")
12
+
13
+
14
+ def cut_mention(text: str) -> tuple[str, Option[str]]:
15
+ left, right = single_split(text, "@")
16
+ return left, Some(right) if right else Nothing()
17
+
18
+
19
+ @dataclass
20
+ class CommandInfo(DataNode):
21
+ name: str
22
+ arguments: str
23
+ mention: Option[str] = field(default_factory=Nothing)
24
+
25
+ @classmethod
26
+ async def compose(cls, text: Text):
27
+ name, arguments = single_split(text, separator=" ")
28
+ name, mention = cut_mention(name)
29
+ return cls(name, arguments, mention)