telegrinder 0.2.1__py3-none-any.whl → 0.3.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 (85) hide show
  1. telegrinder/__init__.py +45 -12
  2. telegrinder/bot/__init__.py +32 -4
  3. telegrinder/bot/cute_types/callback_query.py +60 -146
  4. telegrinder/bot/cute_types/chat_join_request.py +12 -16
  5. telegrinder/bot/cute_types/chat_member_updated.py +15 -105
  6. telegrinder/bot/cute_types/inline_query.py +5 -14
  7. telegrinder/bot/cute_types/message.py +623 -1238
  8. telegrinder/bot/dispatch/__init__.py +40 -3
  9. telegrinder/bot/dispatch/abc.py +8 -1
  10. telegrinder/bot/dispatch/context.py +2 -2
  11. telegrinder/bot/dispatch/dispatch.py +8 -1
  12. telegrinder/bot/dispatch/handler/__init__.py +17 -1
  13. telegrinder/bot/dispatch/handler/audio_reply.py +44 -0
  14. telegrinder/bot/dispatch/handler/base.py +57 -0
  15. telegrinder/bot/dispatch/handler/document_reply.py +44 -0
  16. telegrinder/bot/dispatch/handler/func.py +3 -3
  17. telegrinder/bot/dispatch/handler/media_group_reply.py +43 -0
  18. telegrinder/bot/dispatch/handler/message_reply.py +12 -35
  19. telegrinder/bot/dispatch/handler/photo_reply.py +44 -0
  20. telegrinder/bot/dispatch/handler/sticker_reply.py +37 -0
  21. telegrinder/bot/dispatch/handler/video_reply.py +44 -0
  22. telegrinder/bot/dispatch/process.py +2 -2
  23. telegrinder/bot/dispatch/return_manager/abc.py +11 -8
  24. telegrinder/bot/dispatch/return_manager/callback_query.py +2 -2
  25. telegrinder/bot/dispatch/return_manager/inline_query.py +2 -2
  26. telegrinder/bot/dispatch/return_manager/message.py +3 -3
  27. telegrinder/bot/dispatch/view/__init__.py +2 -1
  28. telegrinder/bot/dispatch/view/abc.py +2 -181
  29. telegrinder/bot/dispatch/view/base.py +200 -0
  30. telegrinder/bot/dispatch/view/callback_query.py +3 -3
  31. telegrinder/bot/dispatch/view/chat_join_request.py +2 -2
  32. telegrinder/bot/dispatch/view/chat_member.py +2 -3
  33. telegrinder/bot/dispatch/view/inline_query.py +2 -2
  34. telegrinder/bot/dispatch/view/message.py +5 -4
  35. telegrinder/bot/dispatch/view/raw.py +4 -3
  36. telegrinder/bot/dispatch/waiter_machine/__init__.py +18 -0
  37. telegrinder/bot/dispatch/waiter_machine/actions.py +10 -0
  38. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +15 -0
  39. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +60 -0
  40. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +49 -0
  41. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +54 -0
  42. telegrinder/bot/dispatch/waiter_machine/hasher/state.py +19 -0
  43. telegrinder/bot/dispatch/waiter_machine/machine.py +88 -101
  44. telegrinder/bot/dispatch/waiter_machine/middleware.py +23 -41
  45. telegrinder/bot/dispatch/waiter_machine/short_state.py +9 -9
  46. telegrinder/bot/polling/polling.py +5 -2
  47. telegrinder/bot/rules/__init__.py +3 -3
  48. telegrinder/bot/rules/abc.py +6 -5
  49. telegrinder/bot/rules/adapter/__init__.py +1 -1
  50. telegrinder/bot/rules/integer.py +1 -1
  51. telegrinder/bot/rules/is_from.py +19 -0
  52. telegrinder/bot/rules/state.py +9 -6
  53. telegrinder/bot/scenario/checkbox.py +5 -5
  54. telegrinder/bot/scenario/choice.py +2 -2
  55. telegrinder/client/aiohttp.py +5 -7
  56. telegrinder/model.py +6 -11
  57. telegrinder/modules.py +16 -25
  58. telegrinder/msgspec_json.py +1 -1
  59. telegrinder/msgspec_utils.py +56 -5
  60. telegrinder/node/base.py +2 -2
  61. telegrinder/node/composer.py +5 -9
  62. telegrinder/node/container.py +6 -1
  63. telegrinder/node/event.py +2 -0
  64. telegrinder/node/polymorphic.py +7 -7
  65. telegrinder/node/rule.py +6 -4
  66. telegrinder/node/scope.py +3 -3
  67. telegrinder/node/source.py +4 -2
  68. telegrinder/node/tools/generator.py +7 -6
  69. telegrinder/rules.py +2 -2
  70. telegrinder/tools/__init__.py +10 -10
  71. telegrinder/tools/functional.py +9 -0
  72. telegrinder/tools/keyboard.py +6 -1
  73. telegrinder/tools/loop_wrapper/loop_wrapper.py +4 -5
  74. telegrinder/tools/magic.py +17 -19
  75. telegrinder/tools/state_storage/__init__.py +3 -3
  76. telegrinder/tools/state_storage/abc.py +12 -10
  77. telegrinder/tools/state_storage/memory.py +9 -6
  78. telegrinder/types/__init__.py +1 -0
  79. telegrinder/types/methods.py +10 -2
  80. telegrinder/types/objects.py +47 -5
  81. {telegrinder-0.2.1.dist-info → telegrinder-0.3.0.dist-info}/METADATA +4 -5
  82. telegrinder-0.3.0.dist-info/RECORD +164 -0
  83. telegrinder-0.2.1.dist-info/RECORD +0 -149
  84. {telegrinder-0.2.1.dist-info → telegrinder-0.3.0.dist-info}/LICENSE +0 -0
  85. {telegrinder-0.2.1.dist-info → telegrinder-0.3.0.dist-info}/WHEEL +0 -0
@@ -107,21 +107,40 @@ class IsSticker(MessageRule):
107
107
  return bool(message.sticker)
108
108
 
109
109
 
110
+ class IsVideoNote(MessageRule):
111
+ async def check(self, message: Message) -> bool:
112
+ return bool(message.video_note)
113
+
114
+
115
+ class IsDocument(MessageRule):
116
+ async def check(self, message: Message) -> bool:
117
+ return bool(message.document)
118
+
119
+
120
+ class IsPhoto(MessageRule):
121
+ async def check(self, message: Message) -> bool:
122
+ return bool(message.photo)
123
+
124
+
110
125
  __all__ = (
111
126
  "IsBot",
112
127
  "IsChat",
113
128
  "IsChatId",
114
129
  "IsDice",
115
130
  "IsDiceEmoji",
131
+ "IsDocument",
116
132
  "IsForum",
117
133
  "IsForward",
118
134
  "IsForwardType",
119
135
  "IsGroup",
120
136
  "IsLanguageCode",
137
+ "IsPhoto",
121
138
  "IsPremium",
122
139
  "IsPrivate",
123
140
  "IsReply",
141
+ "IsSticker",
124
142
  "IsSuperGroup",
125
143
  "IsUser",
126
144
  "IsUserId",
145
+ "IsVideoNote",
127
146
  )
@@ -1,12 +1,15 @@
1
+ import dataclasses
1
2
  import enum
2
3
  import typing
3
4
 
4
5
  from telegrinder.bot.dispatch.context import Context
5
6
  from telegrinder.bot.rules.abc import ABCRule
6
- from telegrinder.node import Source
7
+ from telegrinder.node.source import Source
7
8
 
8
9
  if typing.TYPE_CHECKING:
9
- from telegrinder.tools.state_storage import ABCStateStorage
10
+ from telegrinder.tools.state_storage.abc import ABCStateStorage
11
+
12
+ Payload = typing.TypeVar("Payload")
10
13
 
11
14
 
12
15
  class StateMeta(enum.Enum):
@@ -14,10 +17,10 @@ class StateMeta(enum.Enum):
14
17
  ANY = enum.auto()
15
18
 
16
19
 
17
- class State(ABCRule):
18
- def __init__(self, storage: "ABCStateStorage", key: str | StateMeta = StateMeta.ANY):
19
- self.storage = storage
20
- self.key = key
20
+ @dataclasses.dataclass(frozen=True, slots=True, repr=False)
21
+ class State(ABCRule, typing.Generic[Payload]):
22
+ storage: "ABCStateStorage[Payload]"
23
+ key: str | StateMeta | enum.Enum
21
24
 
22
25
  async def check(self, source: Source, ctx: Context) -> bool:
23
26
  state = await self.storage.get(source.from_user.id)
@@ -2,16 +2,16 @@ import dataclasses
2
2
  import secrets
3
3
  import typing
4
4
 
5
- from telegrinder.bot.cute_types import CallbackQueryCute
6
- from telegrinder.bot.dispatch.waiter_machine import WaiterMachine
5
+ from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
6
+ from telegrinder.bot.dispatch.waiter_machine import StateViewHasher, WaiterMachine
7
7
  from telegrinder.bot.scenario.abc import ABCScenario
8
- from telegrinder.tools import InlineButton, InlineKeyboard
8
+ from telegrinder.tools.keyboard import InlineButton, InlineKeyboard
9
9
  from telegrinder.tools.parse_mode import ParseMode
10
10
  from telegrinder.types.objects import InlineKeyboardMarkup
11
11
 
12
12
  if typing.TYPE_CHECKING:
13
13
  from telegrinder.api import API
14
- from telegrinder.bot.dispatch.view.abc import BaseStateView
14
+ from telegrinder.bot.dispatch.view.base import BaseStateView
15
15
 
16
16
 
17
17
  @dataclasses.dataclass(slots=True)
@@ -124,7 +124,7 @@ class Checkbox(ABCScenario[CallbackQueryCute]):
124
124
  ).unwrap()
125
125
 
126
126
  while True:
127
- q, _ = await self.waiter_machine.wait(view, (api, message.message_id))
127
+ q, _ = await self.waiter_machine.wait(StateViewHasher(view.__class__), message.message_id)
128
128
  should_continue = await self.handle(q)
129
129
  await q.answer(self.CALLBACK_ANSWER)
130
130
  if not should_continue:
@@ -1,11 +1,11 @@
1
1
  import typing
2
2
 
3
- from telegrinder.bot.cute_types import CallbackQueryCute
3
+ from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
4
4
  from telegrinder.bot.scenario.checkbox import Checkbox
5
5
 
6
6
  if typing.TYPE_CHECKING:
7
7
  from telegrinder.api import API
8
- from telegrinder.bot.dispatch.view.abc import BaseStateView
8
+ from telegrinder.bot.dispatch.view.base import BaseStateView
9
9
 
10
10
 
11
11
  class Choice(Checkbox):
@@ -5,8 +5,8 @@ import aiohttp
5
5
  import certifi
6
6
  from aiohttp import ClientSession, TCPConnector
7
7
 
8
+ import telegrinder.msgspec_json as json
8
9
  from telegrinder.client.abc import ABCClient
9
- from telegrinder.modules import JSONModule, json
10
10
 
11
11
  if typing.TYPE_CHECKING:
12
12
  from aiohttp import ClientResponse
@@ -16,12 +16,10 @@ class AiohttpClient(ABCClient):
16
16
  def __init__(
17
17
  self,
18
18
  session: ClientSession | None = None,
19
- json_processing_module: JSONModule | None = None,
20
19
  timeout: aiohttp.ClientTimeout | None = None,
21
20
  **session_params: typing.Any,
22
21
  ) -> None:
23
22
  self.session = session
24
- self.json_processing_module = json_processing_module or json
25
23
  self.session_params = session_params
26
24
  self.timeout = timeout or aiohttp.ClientTimeout(total=0)
27
25
 
@@ -43,7 +41,7 @@ class AiohttpClient(ABCClient):
43
41
  if not self.session:
44
42
  self.session = ClientSession(
45
43
  connector=TCPConnector(ssl=ssl.create_default_context(cafile=certifi.where())),
46
- json_serialize=self.json_processing_module.dumps,
44
+ json_serialize=json.dumps,
47
45
  **self.session_params,
48
46
  )
49
47
  async with self.session.request(
@@ -65,8 +63,8 @@ class AiohttpClient(ABCClient):
65
63
  ) -> dict[str, typing.Any]:
66
64
  response = await self.request_raw(url, method, data, **kwargs)
67
65
  return await response.json(
68
- encoding="utf-8",
69
- loads=self.json_processing_module.loads,
66
+ encoding="UTF-8",
67
+ loads=json.loads,
70
68
  content_type=None,
71
69
  )
72
70
 
@@ -78,7 +76,7 @@ class AiohttpClient(ABCClient):
78
76
  **kwargs: typing.Any,
79
77
  ) -> str:
80
78
  response = await self.request_raw(url, method, data, **kwargs) # type: ignore
81
- return await response.text(encoding="utf-8")
79
+ return await response.text(encoding="UTF-8")
82
80
 
83
81
  async def request_bytes(
84
82
  self,
telegrinder/model.py CHANGED
@@ -16,17 +16,12 @@ if typing.TYPE_CHECKING:
16
16
 
17
17
  T = typing.TypeVar("T")
18
18
 
19
-
20
- def rename_field(name: str) -> str:
21
- if name.endswith("_") and name.removesuffix("_") in keyword.kwlist:
22
- return name.removesuffix("_")
23
- return name if not keyword.iskeyword(name) else name + "_"
24
-
19
+ UnionType: typing.TypeAlias = typing.Annotated[tuple[T, ...], ...]
25
20
 
26
21
  MODEL_CONFIG: typing.Final[dict[str, typing.Any]] = {
27
22
  "omit_defaults": True,
28
23
  "dict": True,
29
- "rename": rename_field,
24
+ "rename": {kw + "_": kw for kw in keyword.kwlist},
30
25
  }
31
26
 
32
27
 
@@ -40,15 +35,15 @@ def full_result(
40
35
  @typing.overload
41
36
  def full_result(
42
37
  result: Result[msgspec.Raw, "APIError"],
43
- full_t: tuple[type[T], ...],
38
+ full_t: UnionType[T],
44
39
  ) -> Result[T, "APIError"]: ...
45
40
 
46
41
 
47
42
  def full_result(
48
43
  result: Result[msgspec.Raw, "APIError"],
49
- full_t: type[T] | tuple[type[T], ...],
50
- ) -> Result[T, "APIError"]:
51
- return result.map(lambda v: decoder.decode(v, type=full_t)) # type: ignore
44
+ full_t: typing.Any,
45
+ ) -> Result[typing.Any, "APIError"]:
46
+ return result.map(lambda v: decoder.decode(v, type=full_t))
52
47
 
53
48
 
54
49
  def get_params(params: dict[str, typing.Any]) -> dict[str, typing.Any]:
telegrinder/modules.py CHANGED
@@ -4,13 +4,6 @@ import typing
4
4
  from choicelib import choice_in_order
5
5
 
6
6
 
7
- @typing.runtime_checkable
8
- class JSONModule(typing.Protocol):
9
- def loads(self, s: str | bytes) -> typing.Any: ...
10
-
11
- def dumps(self, o: typing.Any) -> str: ...
12
-
13
-
14
7
  @typing.runtime_checkable
15
8
  class LoggerModule(typing.Protocol):
16
9
  def debug(self, __msg: object, *args: object, **kwargs: object) -> None: ...
@@ -25,25 +18,23 @@ class LoggerModule(typing.Protocol):
25
18
 
26
19
  def exception(self, __msg: object, *args: object, **kwargs: object) -> None: ...
27
20
 
28
- def set_level(
29
- self,
30
- level: typing.Literal[
31
- "DEBUG",
32
- "INFO",
33
- "WARNING",
34
- "ERROR",
35
- "CRITICAL",
36
- "EXCEPTION",
37
- ],
38
- ) -> None: ...
21
+ if typing.TYPE_CHECKING:
22
+
23
+ def set_level(
24
+ self,
25
+ level: typing.Literal[
26
+ "DEBUG",
27
+ "INFO",
28
+ "WARNING",
29
+ "ERROR",
30
+ "CRITICAL",
31
+ "EXCEPTION",
32
+ ],
33
+ /,
34
+ ) -> None: ...
39
35
 
40
36
 
41
37
  logger: LoggerModule
42
- json: JSONModule = choice_in_order(
43
- ["orjson", "ujson", "hyperjson"],
44
- default="telegrinder.msgspec_json",
45
- do_import=True,
46
- )
47
38
  logging_level = os.getenv("LOGGER_LEVEL", default="DEBUG").upper()
48
39
  logging_module = choice_in_order(["loguru"], default="logging")
49
40
  asyncio_module = choice_in_order(["uvloop"], default="asyncio")
@@ -227,7 +218,7 @@ if asyncio_module == "uvloop":
227
218
  asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) # type: ignore
228
219
 
229
220
 
230
- def _set_logger_level(level):
221
+ def _set_logger_level(level, /):
231
222
  level = level.upper()
232
223
  if logging_module == "logging":
233
224
  import logging
@@ -243,4 +234,4 @@ def _set_logger_level(level):
243
234
  setattr(logger, "set_level", staticmethod(_set_logger_level)) # type: ignore
244
235
 
245
236
 
246
- __all__ = ("json", "logger")
237
+ __all__ = ("LoggerModule", "logger")
@@ -1,6 +1,6 @@
1
1
  import typing
2
2
 
3
- from .msgspec_utils import decoder, encoder
3
+ from telegrinder.msgspec_utils import decoder, encoder
4
4
 
5
5
 
6
6
  def loads(s: str | bytes) -> typing.Any:
@@ -1,5 +1,6 @@
1
1
  import dataclasses
2
2
  import typing
3
+ from contextlib import contextmanager
3
4
 
4
5
  import fntypes.option
5
6
  import fntypes.result
@@ -29,7 +30,6 @@ else:
29
30
  def __instancecheck__(cls, __instance: typing.Any) -> bool:
30
31
  return isinstance(__instance, fntypes.option.Some | fntypes.option.Nothing)
31
32
 
32
-
33
33
  class Option(typing.Generic[Value], metaclass=OptionMeta):
34
34
  pass
35
35
 
@@ -63,9 +63,10 @@ def is_common_type(type_: typing.Any) -> typing.TypeGuard[type[typing.Any]]:
63
63
  def type_check(obj: typing.Any, t: typing.Any) -> bool:
64
64
  return (
65
65
  isinstance(obj, t)
66
- if isinstance(t, type)
67
- and issubclass(t, msgspec.Struct)
68
- else type(obj) in t if isinstance(t, tuple) else type(obj) is t
66
+ if isinstance(t, type) and issubclass(t, msgspec.Struct)
67
+ else type(obj) in t
68
+ if isinstance(t, tuple)
69
+ else type(obj) is t
69
70
  )
70
71
 
71
72
 
@@ -201,6 +202,31 @@ class Decoder:
201
202
  self.dec_hooks,
202
203
  )
203
204
 
205
+ @typing.overload
206
+ def __call__(self, type: type[T]) -> typing.ContextManager[msgspec.json.Decoder[T]]: ...
207
+
208
+ @typing.overload
209
+ def __call__(self, type: typing.Any) -> typing.ContextManager[msgspec.json.Decoder[typing.Any]]: ...
210
+
211
+ @typing.overload
212
+ def __call__(
213
+ self, type: type[T], *, strict: bool = True
214
+ ) -> typing.ContextManager[msgspec.json.Decoder[T]]: ...
215
+
216
+ @typing.overload
217
+ def __call__(
218
+ self, type: typing.Any, *, strict: bool = True
219
+ ) -> typing.ContextManager[msgspec.json.Decoder[typing.Any]]: ...
220
+
221
+ @contextmanager
222
+ def __call__(self, type=object, *, strict=True):
223
+ """Context manager returns the `msgspec.json.Decoder` object with the `dec_hook`."""
224
+
225
+ dec_obj = msgspec.json.Decoder(
226
+ type=typing.Any if type is object else type, strict=strict, dec_hook=self.dec_hook
227
+ )
228
+ yield dec_obj
229
+
204
230
  def add_dec_hook(self, t: T): # type: ignore
205
231
  def decorator(func: DecHook[T]) -> DecHook[T]:
206
232
  return self.dec_hooks.setdefault(get_origin(t), func) # type: ignore
@@ -241,6 +267,9 @@ class Decoder:
241
267
  @typing.overload
242
268
  def decode(self, buf: str | bytes, *, type: type[T]) -> T: ...
243
269
 
270
+ @typing.overload
271
+ def decode(self, buf: str | bytes, *, type: typing.Any) -> typing.Any: ...
272
+
244
273
  @typing.overload
245
274
  def decode(
246
275
  self,
@@ -250,6 +279,15 @@ class Decoder:
250
279
  strict: bool = True,
251
280
  ) -> T: ...
252
281
 
282
+ @typing.overload
283
+ def decode(
284
+ self,
285
+ buf: str | bytes,
286
+ *,
287
+ type: typing.Any,
288
+ strict: bool = True,
289
+ ) -> typing.Any: ...
290
+
253
291
  def decode(self, buf, *, type=object, strict=True):
254
292
  return msgspec.json.decode(
255
293
  buf,
@@ -287,6 +325,19 @@ class Encoder:
287
325
  self.enc_hooks,
288
326
  )
289
327
 
328
+ @contextmanager
329
+ def __call__(
330
+ self,
331
+ *,
332
+ decimal_format: typing.Literal["string", "number"] = "string",
333
+ uuid_format: typing.Literal["canonical", "hex"] = "canonical",
334
+ order: typing.Literal[None, "deterministic", "sorted"] = None,
335
+ ) -> typing.Generator[msgspec.json.Encoder, typing.Any, None]:
336
+ """Context manager returns the `msgspec.json.Encoder` object with the `enc_hook`."""
337
+
338
+ enc_obj = msgspec.json.Encoder(enc_hook=self.enc_hook)
339
+ yield enc_obj
340
+
290
341
  def add_dec_hook(self, t: type[T]):
291
342
  def decorator(func: EncHook[T]) -> EncHook[T]:
292
343
  encode_hook = self.enc_hooks.setdefault(get_origin(t), func)
@@ -344,8 +395,8 @@ __all__ = (
344
395
  "datetime",
345
396
  "decoder",
346
397
  "encoder",
347
- "msgspec_convert",
348
398
  "get_class_annotations",
349
399
  "get_type_hints",
400
+ "msgspec_convert",
350
401
  "msgspec_to_builtins",
351
402
  )
telegrinder/node/base.py CHANGED
@@ -12,8 +12,8 @@ from telegrinder.tools.magic import (
12
12
  ComposeResult: typing.TypeAlias = typing.Awaitable[typing.Any] | typing.AsyncGenerator[typing.Any, None]
13
13
 
14
14
 
15
- def is_node(maybe_node: type[typing.Any]) -> typing.TypeGuard[type["Node"]]:
16
- maybe_node = typing.get_origin(maybe_node) or maybe_node
15
+ def is_node(maybe_node: typing.Any) -> typing.TypeGuard[type["Node"]]:
16
+ maybe_node = maybe_node if isinstance(maybe_node, type) else typing.get_origin(maybe_node)
17
17
  return (
18
18
  isinstance(maybe_node, type)
19
19
  and issubclass(maybe_node, Node)
@@ -4,7 +4,7 @@ import typing
4
4
  from fntypes import Error, Ok, Result
5
5
  from fntypes.error import UnwrapError
6
6
 
7
- from telegrinder.api import API
7
+ from telegrinder.api.api import API
8
8
  from telegrinder.bot.cute_types.update import Update, UpdateCute
9
9
  from telegrinder.bot.dispatch.context import Context
10
10
  from telegrinder.modules import logger
@@ -41,7 +41,7 @@ async def compose_node(
41
41
  async def compose_nodes(
42
42
  nodes: dict[str, type[Node]],
43
43
  ctx: Context,
44
- data: dict[type, typing.Any] | None = None,
44
+ data: dict[type[typing.Any], typing.Any] | None = None,
45
45
  ) -> Result["NodeCollection", ComposeError]:
46
46
  logger.debug("Composing nodes: {!r}...", nodes)
47
47
 
@@ -67,11 +67,7 @@ async def compose_nodes(
67
67
  local_nodes[node_t] = getattr(node_t, GLOBAL_VALUE_KEY)
68
68
  continue
69
69
 
70
- subnodes = {
71
- k: session.value
72
- for k, session in
73
- (local_nodes | event_nodes).items()
74
- }
70
+ subnodes = {k: session.value for k, session in (local_nodes | event_nodes).items()}
75
71
 
76
72
  try:
77
73
  local_nodes[node_t] = await compose_node(node_t, subnodes | data)
@@ -175,11 +171,11 @@ class Composition:
175
171
  case Ok(col):
176
172
  return col
177
173
  case Error(err):
178
- logger.debug(f"Composition failed with error: {err}")
174
+ logger.debug(f"Composition failed with error: {err!r}")
179
175
  return None
180
176
 
181
177
  async def __call__(self, **kwargs: typing.Any) -> typing.Any:
182
- return await self.func(**magic_bundle(self.func, kwargs, start_idx=0, bundle_ctx=False)) # type: ignore
178
+ return await self.func(**magic_bundle(self.func, kwargs, start_idx=0, bundle_ctx=False))
183
179
 
184
180
 
185
181
  __all__ = ("Composition", "NodeCollection", "NodeSession", "compose_node", "compose_nodes")
@@ -12,7 +12,12 @@ class ContainerNode(Node):
12
12
 
13
13
  @classmethod
14
14
  def get_subnodes(cls) -> dict[str, type[Node]]:
15
- return {f"node_{i}": node_t for i, node_t in enumerate(cls.linked_nodes)}
15
+ subnodes = getattr(cls, "subnodes", None)
16
+ if subnodes is None:
17
+ subnodes_dct = {f"node_{i}": node_t for i, node_t in enumerate(cls.linked_nodes)}
18
+ setattr(cls, "subnodes", subnodes_dct)
19
+ return subnodes_dct
20
+ return subnodes
16
21
 
17
22
  @classmethod
18
23
  def link_nodes(cls, linked_nodes: list[type[Node]]) -> type["ContainerNode"]:
telegrinder/node/event.py CHANGED
@@ -65,7 +65,9 @@ if typing.TYPE_CHECKING:
65
65
  EventNode: typing.TypeAlias = typing.Annotated["Dataclass", ...]
66
66
 
67
67
  else:
68
+
68
69
  class EventNode(_EventNode):
69
70
  pass
70
71
 
72
+
71
73
  __all__ = ("EventNode",)
@@ -13,21 +13,21 @@ from telegrinder.tools.magic import get_impls, impl
13
13
  class Polymorphic(Node):
14
14
  @classmethod
15
15
  async def compose(cls, update: UpdateNode, context: Context) -> typing.Any:
16
- logger.debug(f"Composing polimorphic node {cls.__name__}")
16
+ logger.debug(f"Composing polimorphic node {cls.__name__!r}...")
17
17
  scope = getattr(cls, "scope", None)
18
18
  node_ctx = context.get_or_set(CONTEXT_STORE_NODES_KEY, {})
19
19
 
20
- for i, impl in enumerate(get_impls(cls)):
21
- logger.debug("Checking impl {}", impl.__name__)
22
- composition = Composition(impl, True)
20
+ for i, impl_ in enumerate(get_impls(cls)):
21
+ logger.debug("Checking impl {!r}...", impl_.__name__)
22
+ composition = Composition(impl_, True)
23
23
  node_collection = await composition.compose_nodes(update, context)
24
24
  if node_collection is None:
25
- logger.debug("Impl {!r} composition failed", impl.__name__)
25
+ logger.debug("Impl {!r} composition failed!", impl_.__name__)
26
26
  continue
27
27
 
28
28
  # To determine whether this is a right morph, all subnodes must be resolved
29
29
  if scope is NodeScope.PER_EVENT and (cls, i) in node_ctx:
30
- logger.debug("Morph is already cached as per_event node, using its value. Impl {!r} succeeded", impl.__name__)
30
+ logger.debug("Morph is already cached as per_event node, using its value. Impl {!r} succeeded!", impl_.__name__)
31
31
  res: NodeSession = node_ctx[(cls, i)]
32
32
  await node_collection.close_all()
33
33
  return res.value
@@ -40,7 +40,7 @@ class Polymorphic(Node):
40
40
  node_ctx[(cls, i)] = NodeSession(cls, result, {})
41
41
 
42
42
  await node_collection.close_all(with_value=result)
43
- logger.debug("Impl {!r} succeeded with value {}", impl.__name__, result)
43
+ logger.debug("Impl {!r} succeeded with value: {}", impl_.__name__, result)
44
44
  return result
45
45
 
46
46
  raise ComposeError("No implementation found.")
telegrinder/node/rule.py CHANGED
@@ -11,7 +11,7 @@ if typing.TYPE_CHECKING:
11
11
  from telegrinder.bot.rules.abc import ABCRule
12
12
 
13
13
 
14
- class RuleChain(dict[str, typing.Any]):
14
+ class RuleChain(dict[str, typing.Any], Node):
15
15
  dataclass = dict
16
16
  rules: tuple["ABCRule", ...] = ()
17
17
 
@@ -28,7 +28,6 @@ class RuleChain(dict[str, typing.Any]):
28
28
  def __class_getitem__(cls, items: "ABCRule | tuple[ABCRule, ...]", /) -> typing.Self:
29
29
  if not isinstance(items, tuple):
30
30
  items = (items,)
31
- assert all(isinstance(rule, "ABCRule") for rule in items), "All items must be instances of 'ABCRule'."
32
31
  return cls(*items)
33
32
 
34
33
  @staticmethod
@@ -37,6 +36,7 @@ class RuleChain(dict[str, typing.Any]):
37
36
 
38
37
  @classmethod
39
38
  async def compose(cls, update: UpdateNode) -> typing.Any:
39
+ # Hack to avoid circular import
40
40
  globalns = globals()
41
41
  if "check_rule" not in globalns:
42
42
  globalns.update(
@@ -54,7 +54,9 @@ class RuleChain(dict[str, typing.Any]):
54
54
  raise ComposeError(f"Rule {rule!r} failed!")
55
55
 
56
56
  try:
57
- return cls.dataclass(**ctx) # type: ignore
57
+ if dataclasses.is_dataclass(cls.dataclass):
58
+ return cls.dataclass(**{k: ctx[k] for k in cls.__annotations__})
59
+ return cls.dataclass(**ctx)
58
60
  except Exception as exc:
59
61
  raise ComposeError(f"Dataclass validation error: {exc}")
60
62
 
@@ -63,7 +65,7 @@ class RuleChain(dict[str, typing.Any]):
63
65
  return cls
64
66
 
65
67
  @classmethod
66
- def get_subnodes(cls) -> dict:
68
+ def get_subnodes(cls) -> dict[typing.Literal["update"], type[UpdateNode]]:
67
69
  return {"update": UpdateNode}
68
70
 
69
71
  @classmethod
telegrinder/node/scope.py CHANGED
@@ -34,11 +34,11 @@ def global_node(node: T) -> T:
34
34
 
35
35
 
36
36
  __all__ = (
37
+ "GLOBAL",
37
38
  "NodeScope",
38
- "PER_EVENT",
39
39
  "PER_CALL",
40
+ "PER_EVENT",
41
+ "global_node",
40
42
  "per_call",
41
43
  "per_event",
42
- "global_node",
43
- "GLOBAL",
44
44
  )
@@ -39,7 +39,9 @@ class Source(Polymorphic, DataNode):
39
39
  )
40
40
 
41
41
  @impl
42
- async def compose_chat_join_request(cls, chat_join_request: EventNode[ChatJoinRequestCute]) -> typing.Self:
42
+ async def compose_chat_join_request(
43
+ cls, chat_join_request: EventNode[ChatJoinRequestCute]
44
+ ) -> typing.Self:
43
45
  return cls(
44
46
  api=chat_join_request.ctx_api,
45
47
  chat=chat_join_request.chat,
@@ -68,4 +70,4 @@ class UserSource(ScalarNode, User):
68
70
  return source.from_user
69
71
 
70
72
 
71
- __all__ = ("Source", "ChatSource", "UserSource")
73
+ __all__ = ("ChatSource", "Source", "UserSource")
@@ -20,13 +20,13 @@ def error_on_none(value: T | None) -> T:
20
20
 
21
21
 
22
22
  def generate_node(
23
- subnodes: tuple[type["Node"], ...],
24
- func: typing.Callable[..., T],
23
+ subnodes: tuple[type[Node], ...],
24
+ func: typing.Callable[..., typing.Any],
25
25
  casts: tuple[typing.Callable[[typing.Any], typing.Any], ...] = (cast_false_to_none, error_on_none),
26
- ) -> type["Node"]:
27
- async def compose(**kw: typing.Any) -> typing.Any:
26
+ ) -> type[Node]:
27
+ async def compose(cls, **kw) -> typing.Any:
28
28
  args = await ContainerNode.compose(**kw)
29
- result = func(*args)
29
+ result = func(*args) # type: ignore
30
30
  if inspect.isawaitable(result):
31
31
  result = await result
32
32
  for cast in casts:
@@ -34,7 +34,8 @@ def generate_node(
34
34
  return result
35
35
 
36
36
  container = ContainerNode.link_nodes(list(subnodes))
37
- return type("_ContainerNode", (container,), {"compose": compose})
37
+ compose.__annotations__ = container.get_subnodes()
38
+ return type("_ContainerNode", (container,), {"compose": classmethod(compose)})
38
39
 
39
40
 
40
41
  __all__ = ("generate_node",)
telegrinder/rules.py CHANGED
@@ -26,7 +26,6 @@ __all__ = (
26
26
  "InlineQueryMarkup",
27
27
  "InlineQueryRule",
28
28
  "InlineQueryText",
29
- "IsInteger",
30
29
  "IntegerInRange",
31
30
  "InviteLinkByCreator",
32
31
  "InviteLinkName",
@@ -39,6 +38,7 @@ __all__ = (
39
38
  "IsForward",
40
39
  "IsForwardType",
41
40
  "IsGroup",
41
+ "IsInteger",
42
42
  "IsLanguageCode",
43
43
  "IsPremium",
44
44
  "IsPrivate",
@@ -50,11 +50,11 @@ __all__ = (
50
50
  "Markup",
51
51
  "MessageEntities",
52
52
  "MessageRule",
53
+ "NodeRule",
53
54
  "NotRule",
54
55
  "OrRule",
55
56
  "Regex",
56
57
  "RuleEnum",
57
58
  "StartCommand",
58
59
  "Text",
59
- "NodeRule",
60
60
  )