telegrinder 0.1.dev168__py3-none-any.whl → 0.1.dev170__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 (100) 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 +54 -43
  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 +35 -33
  15. telegrinder/bot/dispatch/handler/func.py +34 -13
  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 +37 -45
  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 +77 -35
  28. telegrinder/bot/dispatch/waiter_machine/middleware.py +31 -7
  29. telegrinder/bot/dispatch/waiter_machine/short_state.py +17 -8
  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 +4 -4
  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/choice.py +2 -3
  58. telegrinder/model.py +51 -16
  59. telegrinder/modules.py +14 -6
  60. telegrinder/msgspec_utils.py +67 -23
  61. telegrinder/node/__init__.py +26 -8
  62. telegrinder/node/attachment.py +13 -9
  63. telegrinder/node/base.py +27 -14
  64. telegrinder/node/callback_query.py +18 -0
  65. telegrinder/node/command.py +29 -0
  66. telegrinder/node/composer.py +119 -30
  67. telegrinder/node/me.py +14 -0
  68. telegrinder/node/message.py +2 -4
  69. telegrinder/node/polymorphic.py +44 -0
  70. telegrinder/node/rule.py +26 -22
  71. telegrinder/node/scope.py +36 -0
  72. telegrinder/node/source.py +37 -10
  73. telegrinder/node/text.py +11 -5
  74. telegrinder/node/tools/__init__.py +2 -2
  75. telegrinder/node/tools/generator.py +6 -6
  76. telegrinder/tools/__init__.py +8 -13
  77. telegrinder/tools/buttons.py +23 -17
  78. telegrinder/tools/error_handler/error_handler.py +11 -14
  79. telegrinder/tools/formatting/__init__.py +0 -6
  80. telegrinder/tools/formatting/html.py +10 -12
  81. telegrinder/tools/formatting/links.py +0 -5
  82. telegrinder/tools/formatting/spec_html_formats.py +0 -11
  83. telegrinder/tools/global_context/abc.py +1 -3
  84. telegrinder/tools/global_context/global_context.py +6 -16
  85. telegrinder/tools/i18n/simple.py +1 -3
  86. telegrinder/tools/kb_set/yaml.py +1 -2
  87. telegrinder/tools/keyboard.py +7 -8
  88. telegrinder/tools/limited_dict.py +1 -1
  89. telegrinder/tools/loop_wrapper/loop_wrapper.py +6 -5
  90. telegrinder/tools/magic.py +27 -5
  91. telegrinder/types/__init__.py +20 -0
  92. telegrinder/types/enums.py +37 -31
  93. telegrinder/types/methods.py +608 -327
  94. telegrinder/types/objects.py +1139 -716
  95. {telegrinder-0.1.dev168.dist-info → telegrinder-0.1.dev170.dist-info}/LICENSE +1 -1
  96. {telegrinder-0.1.dev168.dist-info → telegrinder-0.1.dev170.dist-info}/METADATA +6 -5
  97. telegrinder-0.1.dev170.dist-info/RECORD +143 -0
  98. telegrinder/bot/dispatch/composition.py +0 -88
  99. telegrinder-0.1.dev168.dist-info/RECORD +0 -137
  100. {telegrinder-0.1.dev168.dist-info → telegrinder-0.1.dev170.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")
@@ -1,7 +1,6 @@
1
1
  import typing
2
2
 
3
3
  from telegrinder.bot.cute_types import CallbackQueryCute
4
- from telegrinder.bot.dispatch.waiter_machine import WaiterMachine
5
4
 
6
5
  from .checkbox import Checkbox
7
6
 
@@ -10,7 +9,7 @@ if typing.TYPE_CHECKING:
10
9
  from telegrinder.bot.dispatch.view.abc import BaseStateView
11
10
 
12
11
 
13
- class SingleChoice(Checkbox):
12
+ class Choice(Checkbox):
14
13
  async def handle(self, cb: CallbackQueryCute) -> bool:
15
14
  code = cb.data.unwrap().replace(self.random_code + "/", "", 1)
16
15
  if code == "ready":
@@ -43,4 +42,4 @@ class SingleChoice(Checkbox):
43
42
  return tuple(choices.keys())[tuple(choices.values()).index(True)], m_id
44
43
 
45
44
 
46
- __all__ = ("SingleChoice",)
45
+ __all__ = ("Choice",)
telegrinder/model.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import dataclasses
2
2
  import enum
3
+ import keyword
3
4
  import secrets
4
5
  import typing
5
6
  from datetime import datetime
@@ -19,7 +20,7 @@ T = typing.TypeVar("T")
19
20
  MODEL_CONFIG: typing.Final[dict[str, typing.Any]] = {
20
21
  "omit_defaults": True,
21
22
  "dict": True,
22
- "rename": {"from_": "from"},
23
+ "rename": {kw + "_": kw for kw in keyword.kwlist},
23
24
  }
24
25
 
25
26
 
@@ -45,14 +46,17 @@ def full_result(
45
46
 
46
47
 
47
48
  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
- }
49
+ validated_params = {}
50
+ for k, v in (
51
+ *params.pop("other", {}).items(),
52
+ *params.items(),
53
+ ):
54
+ if isinstance(v, Proxy):
55
+ v = v.get()
56
+ if k == "self" or type(v) in (NoneType, Nothing):
57
+ continue
58
+ validated_params[k] = v.unwrap() if isinstance(v, Some) else v
59
+ return validated_params
56
60
 
57
61
 
58
62
  class Model(msgspec.Struct, **MODEL_CONFIG):
@@ -69,9 +73,7 @@ class Model(msgspec.Struct, **MODEL_CONFIG):
69
73
  if "model_as_dict" not in self.__dict__:
70
74
  self.__dict__["model_as_dict"] = msgspec.structs.asdict(self)
71
75
  return {
72
- key: value
73
- for key, value in self.__dict__["model_as_dict"].items()
74
- if key not in exclude_fields
76
+ key: value for key, value in self.__dict__["model_as_dict"].items() if key not in exclude_fields
75
77
  }
76
78
 
77
79
 
@@ -82,7 +84,7 @@ class DataConverter:
82
84
  def __repr__(self) -> str:
83
85
  return "<{}: {}>".format(
84
86
  self.__class__.__name__,
85
- ", ".join(f"{k}={v!r}" for k, v in self.converters),
87
+ ", ".join(f"{k}={v.__name__!r}" for k, v in self.converters.items()),
86
88
  )
87
89
 
88
90
  @property
@@ -129,9 +131,7 @@ class DataConverter:
129
131
  serialize: bool = True,
130
132
  ) -> dict[str, typing.Any]:
131
133
  return {
132
- k: self(v, serialize=serialize)
133
- for k, v in data.items()
134
- if type(v) not in (NoneType, Nothing)
134
+ k: self(v, serialize=serialize) for k, v in data.items() if type(v) not in (NoneType, Nothing)
135
135
  }
136
136
 
137
137
  def convert_lst(
@@ -159,8 +159,43 @@ class DataConverter:
159
159
  return data
160
160
 
161
161
 
162
+ class Proxy:
163
+ def __init__(self, cfg: "_ProxiedDict", key: str):
164
+ self.key = key
165
+ self.cfg = cfg
166
+
167
+ def get(self) -> typing.Any | None:
168
+ return self.cfg._defaults.get(self.key)
169
+
170
+
171
+ class _ProxiedDict(typing.Generic[T]):
172
+ def __init__(self, tp: type[T]) -> None:
173
+ self.type = tp
174
+ self._defaults = {}
175
+
176
+ def __setattribute__(self, name: str, value: typing.Any, /) -> None:
177
+ self._defaults[name] = value
178
+
179
+ def __getitem__(self, key: str, /) -> None:
180
+ return Proxy(self, key) # type: ignore
181
+
182
+ def __setitem__(self, key: str, value: typing.Any, /) -> None:
183
+ self._defaults[key] = value
184
+
185
+
186
+ if typing.TYPE_CHECKING:
187
+
188
+ def ProxiedDict(typed_dct: type[T]) -> T | _ProxiedDict[T]: # noqa: N802
189
+ ...
190
+
191
+ else:
192
+ ProxiedDict = _ProxiedDict
193
+
194
+
162
195
  __all__ = (
196
+ "Proxy",
163
197
  "DataConverter",
198
+ "ProxiedDict",
164
199
  "MODEL_CONFIG",
165
200
  "Model",
166
201
  "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()
@@ -1,6 +1,7 @@
1
1
  import typing
2
2
 
3
3
  import fntypes.option
4
+ import fntypes.result
4
5
  import msgspec
5
6
  from fntypes.co import Error, Ok, Result, Variative
6
7
 
@@ -8,10 +9,12 @@ if typing.TYPE_CHECKING:
8
9
  from datetime import datetime
9
10
 
10
11
  from fntypes.option import Option
12
+ from fntypes.result import Result
11
13
  else:
12
14
  from datetime import datetime as dt
13
15
 
14
16
  Value = typing.TypeVar("Value")
17
+ Err = typing.TypeVar("Err")
15
18
 
16
19
  datetime = type("datetime", (dt,), {})
17
20
 
@@ -19,14 +22,22 @@ else:
19
22
  def __instancecheck__(cls, __instance: typing.Any) -> bool:
20
23
  return isinstance(__instance, fntypes.option.Some | fntypes.option.Nothing)
21
24
 
25
+ class ResultMeta(type):
26
+ def __instancecheck__(cls, __instance: typing.Any) -> bool:
27
+ return isinstance(__instance, fntypes.result.Ok | fntypes.result.Error)
28
+
22
29
  class Option(typing.Generic[Value], metaclass=OptionMeta):
23
30
  pass
24
31
 
32
+ class Result(typing.Generic[Value, Err], metaclass=ResultMeta):
33
+ pass
34
+
25
35
 
26
36
  T = typing.TypeVar("T")
37
+ Type = typing.TypeVar("Type", bound=type | typing.Any)
27
38
  Ts = typing.TypeVarTuple("Ts")
28
39
 
29
- DecHook: typing.TypeAlias = typing.Callable[[type[T], typing.Any], object]
40
+ DecHook: typing.TypeAlias = typing.Callable[[type[T], typing.Any], typing.Any]
30
41
  EncHook: typing.TypeAlias = typing.Callable[[T], typing.Any]
31
42
 
32
43
  Nothing: typing.Final[fntypes.option.Nothing] = fntypes.option.Nothing()
@@ -40,18 +51,52 @@ def repr_type(t: type) -> str:
40
51
  return getattr(t, "__name__", repr(get_origin(t)))
41
52
 
42
53
 
43
- def msgspec_convert(obj: typing.Any, t: type[T]) -> Result[T, msgspec.ValidationError]:
54
+ def msgspec_convert(obj: typing.Any, t: type[T]) -> Result[T, str]:
44
55
  try:
45
56
  return Ok(decoder.convert(obj, type=t, strict=True))
46
- except msgspec.ValidationError as exc:
47
- return Error(exc)
57
+ except msgspec.ValidationError:
58
+ return Error(
59
+ "Expected object of type `{}`, got `{}`.".format(
60
+ repr_type(t),
61
+ repr_type(type(obj)),
62
+ )
63
+ )
48
64
 
49
65
 
50
66
  def option_dec_hook(tp: type[Option[typing.Any]], obj: typing.Any) -> Option[typing.Any]:
51
- if obj is None:
52
- return Nothing
67
+ orig_type = get_origin(tp)
53
68
  (value_type,) = typing.get_args(tp) or (typing.Any,)
54
- return msgspec_convert({"value": obj}, fntypes.option.Some[value_type]).unwrap()
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())
73
+
74
+
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))}`.")
80
+
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)
85
+
86
+ if orig_type is Ok and "ok" in obj:
87
+ return Ok(msgspec_convert(obj["ok"], first_type).unwrap())
88
+
89
+ if orig_type is Error and "error" in obj:
90
+ return Error(msgspec_convert(obj["error"], first_type).unwrap())
91
+
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())
98
+
99
+ raise msgspec.ValidationError(f"Cannot parse object `{obj!r}` to Result.")
55
100
 
56
101
 
57
102
  def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
@@ -67,9 +112,7 @@ def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
67
112
  reverse = False
68
113
 
69
114
  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
- }
115
+ struct_fields_match_sums = {m: len(m.__struct_fields__) for m in struct_fields_match_sums}
73
116
  reverse = True
74
117
 
75
118
  union_types = (
@@ -123,9 +166,14 @@ class Decoder:
123
166
 
124
167
  def __init__(self) -> None:
125
168
  self.dec_hooks: dict[typing.Any, DecHook[typing.Any]] = {
169
+ Result: result_dec_hook,
126
170
  Option: option_dec_hook,
127
171
  Variative: variative_dec_hook,
128
172
  datetime: lambda t, obj: t.fromtimestamp(obj),
173
+ fntypes.result.Error: result_dec_hook,
174
+ fntypes.result.Ok: result_dec_hook,
175
+ fntypes.option.Some: option_dec_hook,
176
+ fntypes.option.Nothing: option_dec_hook,
129
177
  }
130
178
 
131
179
  def __repr__(self) -> str:
@@ -144,8 +192,7 @@ class Decoder:
144
192
  origin_type = t if isinstance((t := get_origin(tp)), type) else type(t)
145
193
  if origin_type not in self.dec_hooks:
146
194
  raise TypeError(
147
- f"Unknown type `{repr_type(origin_type)}`. "
148
- "You can implement decode hook for this type."
195
+ f"Unknown type `{repr_type(origin_type)}`. You can implement decode hook for this type."
149
196
  )
150
197
  return self.dec_hooks[origin_type](tp, obj)
151
198
 
@@ -173,7 +220,7 @@ class Decoder:
173
220
  def decode(self, buf: str | bytes) -> typing.Any: ...
174
221
 
175
222
  @typing.overload
176
- def decode(self, buf: str | bytes, *, strict: bool = True) -> typing.Any: ...
223
+ def decode(self, buf: str | bytes, *, type: type[T]) -> T: ...
177
224
 
178
225
  @typing.overload
179
226
  def decode(
@@ -184,16 +231,10 @@ class Decoder:
184
231
  strict: bool = True,
185
232
  ) -> T: ...
186
233
 
187
- def decode(
188
- self,
189
- buf: str | bytes,
190
- *,
191
- type: type[T] = typing.Any, # type: ignore
192
- strict: bool = True,
193
- ) -> T:
234
+ def decode(self, buf, *, type=object, strict=True):
194
235
  return msgspec.json.decode(
195
236
  buf,
196
- type=type,
237
+ type=typing.Any if type is object else type,
197
238
  strict=strict,
198
239
  dec_hook=self.dec_hook,
199
240
  )
@@ -219,6 +260,10 @@ class Encoder:
219
260
  self.enc_hooks: dict[typing.Any, EncHook[typing.Any]] = {
220
261
  fntypes.option.Some: lambda opt: opt.value,
221
262
  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
+ },
222
267
  Variative: lambda variative: variative.v,
223
268
  datetime: lambda date: int(date.timestamp()),
224
269
  }
@@ -240,8 +285,7 @@ class Encoder:
240
285
  origin_type = get_origin(obj.__class__)
241
286
  if origin_type not in self.enc_hooks:
242
287
  raise NotImplementedError(
243
- "Not implemented encode hook for "
244
- f"object of type `{repr_type(origin_type)}`."
288
+ f"Not implemented encode hook for object of type `{repr_type(origin_type)}`."
245
289
  )
246
290
  return self.enc_hooks[origin_type](obj)
247
291
 
@@ -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",)