telegrinder 0.1.dev166__py3-none-any.whl → 0.1.dev167__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 (62) hide show
  1. telegrinder/__init__.py +0 -2
  2. telegrinder/bot/__init__.py +0 -2
  3. telegrinder/bot/bot.py +1 -3
  4. telegrinder/bot/cute_types/base.py +3 -12
  5. telegrinder/bot/cute_types/callback_query.py +1 -3
  6. telegrinder/bot/cute_types/chat_join_request.py +3 -1
  7. telegrinder/bot/cute_types/chat_member_updated.py +3 -1
  8. telegrinder/bot/cute_types/message.py +10 -31
  9. telegrinder/bot/cute_types/utils.py +1 -3
  10. telegrinder/bot/dispatch/__init__.py +1 -2
  11. telegrinder/bot/dispatch/composition.py +1 -3
  12. telegrinder/bot/dispatch/dispatch.py +1 -3
  13. telegrinder/bot/dispatch/handler/func.py +3 -10
  14. telegrinder/bot/dispatch/return_manager/abc.py +9 -13
  15. telegrinder/bot/dispatch/return_manager/message.py +5 -7
  16. telegrinder/bot/dispatch/view/abc.py +1 -3
  17. telegrinder/bot/dispatch/view/box.py +3 -11
  18. telegrinder/bot/dispatch/view/raw.py +2 -6
  19. telegrinder/bot/dispatch/waiter_machine/__init__.py +1 -2
  20. telegrinder/bot/dispatch/waiter_machine/machine.py +35 -74
  21. telegrinder/bot/dispatch/waiter_machine/middleware.py +12 -5
  22. telegrinder/bot/dispatch/waiter_machine/short_state.py +6 -6
  23. telegrinder/bot/polling/polling.py +2 -6
  24. telegrinder/bot/rules/adapter/event.py +1 -3
  25. telegrinder/bot/rules/callback_data.py +3 -1
  26. telegrinder/bot/rules/fuzzy.py +1 -2
  27. telegrinder/bot/rules/is_from.py +6 -4
  28. telegrinder/bot/rules/markup.py +1 -2
  29. telegrinder/bot/rules/mention.py +1 -4
  30. telegrinder/bot/rules/regex.py +1 -2
  31. telegrinder/bot/rules/rule_enum.py +1 -3
  32. telegrinder/bot/rules/start.py +1 -3
  33. telegrinder/bot/scenario/checkbox.py +1 -5
  34. telegrinder/client/aiohttp.py +1 -3
  35. telegrinder/model.py +4 -3
  36. telegrinder/modules.py +1 -3
  37. telegrinder/msgspec_utils.py +1 -3
  38. telegrinder/node/attachment.py +18 -14
  39. telegrinder/node/base.py +4 -11
  40. telegrinder/node/composer.py +1 -3
  41. telegrinder/node/message.py +3 -1
  42. telegrinder/node/source.py +3 -1
  43. telegrinder/node/text.py +3 -1
  44. telegrinder/tools/__init__.py +2 -0
  45. telegrinder/tools/buttons.py +4 -6
  46. telegrinder/tools/error_handler/abc.py +1 -3
  47. telegrinder/tools/error_handler/error.py +3 -6
  48. telegrinder/tools/error_handler/error_handler.py +17 -13
  49. telegrinder/tools/formatting/html.py +2 -6
  50. telegrinder/tools/formatting/links.py +1 -3
  51. telegrinder/tools/global_context/abc.py +1 -3
  52. telegrinder/tools/global_context/global_context.py +13 -31
  53. telegrinder/tools/i18n/middleware/base.py +1 -3
  54. telegrinder/tools/limited_dict.py +27 -0
  55. telegrinder/tools/loop_wrapper/loop_wrapper.py +3 -7
  56. telegrinder/types/__init__.py +30 -0
  57. telegrinder/types/objects.py +4 -4
  58. telegrinder/verification_utils.py +2 -1
  59. {telegrinder-0.1.dev166.dist-info → telegrinder-0.1.dev167.dist-info}/METADATA +1 -1
  60. {telegrinder-0.1.dev166.dist-info → telegrinder-0.1.dev167.dist-info}/RECORD +62 -61
  61. {telegrinder-0.1.dev166.dist-info → telegrinder-0.1.dev167.dist-info}/LICENSE +0 -0
  62. {telegrinder-0.1.dev166.dist-info → telegrinder-0.1.dev167.dist-info}/WHEEL +0 -0
@@ -41,19 +41,20 @@ class WaiterMiddleware(ABCMiddleware[EventType]):
41
41
  short_state: "ShortState[EventType] | None" = self.machine.storage[view_name].get(key)
42
42
  if not short_state:
43
43
  return True
44
-
44
+
45
+ preset_context = Context(short_state=short_state)
45
46
  if (
46
47
  short_state.expiration_date is not None
47
48
  and datetime.datetime.now() >= short_state.expiration_date
48
49
  ):
49
- await self.machine.drop(self.view, short_state.key)
50
+ await self.machine.drop(self.view, short_state.key, ctx.raw_update, **preset_context.copy())
50
51
  return True
51
52
 
52
53
  handler = FuncHandler(
53
54
  self.pass_runtime,
54
55
  list(short_state.rules),
55
56
  dataclass=None,
56
- preset_context=Context(short_state=short_state),
57
+ preset_context=preset_context,
57
58
  )
58
59
  result = await handler.check(event.ctx_api, ctx.raw_update, ctx)
59
60
 
@@ -63,14 +64,20 @@ class WaiterMiddleware(ABCMiddleware[EventType]):
63
64
  elif short_state.default_behaviour is not None:
64
65
  await self.machine.call_behaviour(
65
66
  self.view,
66
- short_state.default_behaviour,
67
67
  event,
68
+ ctx.raw_update,
69
+ behaviour=short_state.default_behaviour,
68
70
  **handler.preset_context,
69
71
  )
70
72
 
71
73
  return False
72
74
 
73
- async def pass_runtime(self, event: EventType, short_state: "ShortState[EventType]", ctx: Context) -> None:
75
+ async def pass_runtime(
76
+ self,
77
+ event: EventType,
78
+ short_state: "ShortState[EventType]",
79
+ ctx: Context,
80
+ ) -> None:
74
81
  setattr(short_state.event, "context", (event, ctx))
75
82
  short_state.event.set()
76
83
 
@@ -7,13 +7,15 @@ from telegrinder.api import ABCAPI
7
7
  from telegrinder.bot.cute_types import BaseCute
8
8
  from telegrinder.bot.dispatch.handler.abc import ABCHandler
9
9
  from telegrinder.bot.rules.abc import ABCRule
10
+ from telegrinder.model import Model
10
11
 
11
12
  if typing.TYPE_CHECKING:
12
13
  from .machine import Identificator
13
14
 
15
+ T = typing.TypeVar("T", bound=Model)
14
16
  EventModel = typing.TypeVar("EventModel", bound=BaseCute)
15
17
 
16
- Behaviour: typing.TypeAlias = ABCHandler | None
18
+ Behaviour: typing.TypeAlias = ABCHandler[T] | None
17
19
 
18
20
 
19
21
  @dataclasses.dataclass
@@ -26,14 +28,12 @@ class ShortState(typing.Generic[EventModel]):
26
28
  expiration: dataclasses.InitVar[datetime.timedelta | None] = dataclasses.field(
27
29
  default=None,
28
30
  )
29
- default_behaviour: Behaviour | None = dataclasses.field(default=None)
30
- on_drop_behaviour: Behaviour | None = dataclasses.field(default=None)
31
+ default_behaviour: Behaviour[EventModel] | None = dataclasses.field(default=None)
32
+ on_drop_behaviour: Behaviour[EventModel] | None = dataclasses.field(default=None)
31
33
  expiration_date: datetime.datetime | None = dataclasses.field(init=False)
32
34
 
33
35
  def __post_init__(self, expiration: datetime.timedelta | None = None) -> None:
34
- self.expiration_date = (
35
- (datetime.datetime.now() - expiration) if expiration is not None else None
36
- )
36
+ self.expiration_date = (datetime.datetime.now() - expiration) if expiration is not None else None
37
37
 
38
38
 
39
39
  __all__ = ("ShortState",)
@@ -29,9 +29,7 @@ class Polling(ABCPolling):
29
29
  include_updates=include_updates,
30
30
  exclude_updates=exclude_updates,
31
31
  )
32
- self.reconnection_timeout = (
33
- 5 if reconnection_timeout < 0 else reconnection_timeout
34
- )
32
+ self.reconnection_timeout = 5 if reconnection_timeout < 0 else reconnection_timeout
35
33
  self.max_reconnetions = 10 if max_reconnetions < 0 else max_reconnetions
36
34
  self.offset = offset
37
35
  self._stop = False
@@ -62,9 +60,7 @@ class Polling(ABCPolling):
62
60
 
63
61
  if include_updates and exclude_updates:
64
62
  allowed_updates = [
65
- x
66
- for x in allowed_updates
67
- if x in include_updates and x not in exclude_updates
63
+ x for x in allowed_updates if x in include_updates and x not in exclude_updates
68
64
  ]
69
65
  elif exclude_updates:
70
66
  allowed_updates = [x for x in allowed_updates if x not in exclude_updates]
@@ -45,9 +45,7 @@ class EventAdapter(ABCAdapter[Update, CuteT]):
45
45
  AdapterError(f"Update is not an {self.event!r}."),
46
46
  )
47
47
  return Ok(
48
- self.cute_model.from_update(
49
- update_dct[self.event].unwrap(), bound_api=api
50
- ),
48
+ self.cute_model.from_update(update_dct[self.event].unwrap(), bound_api=api),
51
49
  )
52
50
  event = update_dct[update.update_type.unwrap()].unwrap()
53
51
  if not update.update_type or not issubclass(event.__class__, self.event):
@@ -16,7 +16,9 @@ from .markup import Markup, PatternLike, check_string
16
16
 
17
17
  CallbackQuery: typing.TypeAlias = CallbackQueryCute
18
18
  Validator: typing.TypeAlias = typing.Callable[[typing.Any], bool | typing.Awaitable[bool]]
19
- MapDict: typing.TypeAlias = dict[str, "typing.Any | type[typing.Any] | Validator | list[MapDict] | MapDict"]
19
+ MapDict: typing.TypeAlias = dict[
20
+ str, "typing.Any | type[typing.Any] | Validator | list[MapDict] | MapDict"
21
+ ]
20
22
  CallbackMap: typing.TypeAlias = list[tuple[str, "typing.Any | type | Validator | CallbackMap"]]
21
23
  CallbackMapStrict: typing.TypeAlias = list[tuple[str, "Validator | CallbackMapStrict"]]
22
24
 
@@ -15,8 +15,7 @@ class FuzzyText(TextMessageRule):
15
15
 
16
16
  async def check(self, message: Message, ctx: Context) -> bool:
17
17
  match = max(
18
- difflib.SequenceMatcher(a=message.text.unwrap(), b=text).ratio()
19
- for text in self.texts
18
+ difflib.SequenceMatcher(a=message.text.unwrap(), b=text).ratio() for text in self.texts
20
19
  )
21
20
  if match < self.min_ratio:
22
21
  return False
@@ -42,7 +42,9 @@ class IsForward(MessageRule):
42
42
 
43
43
 
44
44
  class IsForwardType(MessageRule, requires=[IsForward()]):
45
- def __init__(self, fwd_type: typing.Literal["user", "hidden_user", "chat", "channel"], /) -> None:
45
+ def __init__(
46
+ self, fwd_type: typing.Literal["user", "hidden_user", "chat", "channel"], /
47
+ ) -> None:
46
48
  self.fwd_type = fwd_type
47
49
 
48
50
  async def check(self, message: Message, ctx: Context) -> bool:
@@ -80,8 +82,8 @@ class IsLanguageCode(ABCRule[T], requires=[HasFrom()]):
80
82
 
81
83
  async def check(self, event: UpdateCute, ctx: Context) -> bool:
82
84
  return (
83
- get_from_user(event.incoming_update.unwrap())
84
- .language_code.unwrap_or_none() in self.lang_codes
85
+ get_from_user(event.incoming_update.unwrap()).language_code.unwrap_or_none()
86
+ in self.lang_codes
85
87
  )
86
88
 
87
89
 
@@ -131,7 +133,7 @@ class IsDiceEmoji(MessageRule, requires=[HasDice()]):
131
133
  self.dice_emoji = dice_emoji
132
134
 
133
135
  async def check(self, message: Message, ctx: Context) -> bool:
134
- return message.dice.unwrap().emoji == self.dice_emoji
136
+ return message.dice.unwrap().emoji == self.dice_emoji
135
137
 
136
138
 
137
139
  __all__ = (
@@ -28,8 +28,7 @@ class Markup(TextMessageRule):
28
28
  if not isinstance(patterns, list):
29
29
  patterns = [patterns]
30
30
  self.patterns = [
31
- vbml.Pattern(pattern) if isinstance(pattern, str) else pattern
32
- for pattern in patterns
31
+ vbml.Pattern(pattern) if isinstance(pattern, str) else pattern for pattern in patterns
33
32
  ]
34
33
 
35
34
  async def check(self, message: Message, ctx: Context) -> bool:
@@ -8,10 +8,7 @@ class HasMention(TextMessageRule):
8
8
  async def check(self, message: Message, ctx: Context) -> bool:
9
9
  if not message.entities.unwrap_or_none():
10
10
  return False
11
- return any(
12
- entity.type == MessageEntityType.MENTION
13
- for entity in message.entities.unwrap()
14
- )
11
+ return any(entity.type == MessageEntityType.MENTION for entity in message.entities.unwrap())
15
12
 
16
13
 
17
14
  __all__ = ("HasMention",)
@@ -19,8 +19,7 @@ class Regex(TextMessageRule):
19
19
  self.regexp.append(re.compile(regex))
20
20
  case _:
21
21
  self.regexp.extend(
22
- re.compile(regexp) if isinstance(regexp, str) else regexp
23
- for regexp in regexp
22
+ re.compile(regexp) if isinstance(regexp, str) else regexp for regexp in regexp
24
23
  )
25
24
 
26
25
  async def check(self, message: Message, ctx: Context) -> bool:
@@ -21,9 +21,7 @@ class RuleEnum(ABCRule[T]):
21
21
  __enum__: list[RuleEnumState]
22
22
 
23
23
  def __init_subclass__(cls, *args, **kwargs):
24
- new_attributes = (
25
- set(cls.__dict__) - set(RuleEnum.__dict__) - {"__enum__", "__init__"}
26
- )
24
+ new_attributes = set(cls.__dict__) - set(RuleEnum.__dict__) - {"__enum__", "__init__"}
27
25
  enum_lst: list[RuleEnumState] = []
28
26
 
29
27
  self = cls.__new__(cls)
@@ -30,9 +30,7 @@ class StartCommand(
30
30
 
31
31
  async def check(self, _: Message, ctx: Context) -> bool:
32
32
  param: str | None = ctx.pop("param", None)
33
- validated_param = (
34
- self.validator(param) if self.validator and param is not None else param
35
- )
33
+ validated_param = self.validator(param) if self.validator and param is not None else param
36
34
 
37
35
  if self.param_required and validated_param is None:
38
36
  return False
@@ -69,11 +69,7 @@ class Checkbox(ABCScenario[CallbackQueryCute]):
69
69
  choice = choices.pop(0)
70
70
  kb.add(
71
71
  InlineButton(
72
- text=(
73
- choice.default_text
74
- if not choice.is_picked
75
- else choice.picked_text
76
- ),
72
+ text=(choice.default_text if not choice.is_picked else choice.picked_text),
77
73
  callback_data=self.random_code + "/" + choice.code,
78
74
  )
79
75
  )
@@ -42,9 +42,7 @@ class AiohttpClient(ABCClient):
42
42
  ) -> "ClientResponse":
43
43
  if not self.session:
44
44
  self.session = ClientSession(
45
- connector=TCPConnector(
46
- ssl=ssl.create_default_context(cafile=certifi.where())
47
- ),
45
+ connector=TCPConnector(ssl=ssl.create_default_context(cafile=certifi.where())),
48
46
  json_serialize=self.json_processing_module.dumps,
49
47
  **self.session_params,
50
48
  )
telegrinder/model.py CHANGED
@@ -10,11 +10,11 @@ from fntypes.co import Nothing, Result, Some
10
10
 
11
11
  from .msgspec_utils import decoder, encoder, get_origin
12
12
 
13
- T = typing.TypeVar("T")
14
-
15
13
  if typing.TYPE_CHECKING:
16
14
  from telegrinder.api.error import APIError
17
15
 
16
+ T = typing.TypeVar("T")
17
+
18
18
 
19
19
  MODEL_CONFIG: typing.Final[dict[str, typing.Any]] = {
20
20
  "omit_defaults": True,
@@ -25,7 +25,8 @@ MODEL_CONFIG: typing.Final[dict[str, typing.Any]] = {
25
25
 
26
26
  @typing.overload
27
27
  def full_result(
28
- result: Result[msgspec.Raw, "APIError"], full_t: type[T]
28
+ result: Result[msgspec.Raw, "APIError"],
29
+ full_t: type[T],
29
30
  ) -> Result[T, "APIError"]: ...
30
31
 
31
32
 
telegrinder/modules.py CHANGED
@@ -147,9 +147,7 @@ elif logging_module == "logging":
147
147
  for level, settings in LEVEL_SETTINGS.items():
148
148
  fmt = FORMAT
149
149
  for name, color in settings.items():
150
- fmt = fmt.replace(f"<{name}>", COLORS[color]).replace(
151
- f"</{name}>", COLORS["reset"]
152
- )
150
+ fmt = fmt.replace(f"<{name}>", COLORS[color]).replace(f"</{name}>", COLORS["reset"])
153
151
  LEVEL_FORMATS[level] = fmt
154
152
 
155
153
  class TelegrinderLoggingFormatter(logging.Formatter):
@@ -66,9 +66,7 @@ def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
66
66
  union_types = tuple(t for t in union_types if t not in struct_fields_match_sums)
67
67
  reverse = False
68
68
 
69
- if len(set(struct_fields_match_sums.values())) != len(
70
- struct_fields_match_sums.values()
71
- ):
69
+ if len(set(struct_fields_match_sums.values())) != len(struct_fields_match_sums.values()):
72
70
  struct_fields_match_sums = {
73
71
  m: len(m.__struct_fields__) for m in struct_fields_match_sums
74
72
  }
@@ -14,21 +14,15 @@ from .message import MessageNode
14
14
  class Attachment(DataNode):
15
15
  attachment_type: typing.Literal["audio", "document", "photo", "poll", "video"]
16
16
  _: dataclasses.KW_ONLY
17
- audio: Option[telegrinder.types.Audio] = dataclasses.field(
18
- default_factory=lambda: Nothing()
19
- )
17
+ audio: Option[telegrinder.types.Audio] = dataclasses.field(default_factory=lambda: Nothing())
20
18
  document: Option[telegrinder.types.Document] = dataclasses.field(
21
19
  default_factory=lambda: Nothing()
22
20
  )
23
21
  photo: Option[list[telegrinder.types.PhotoSize]] = dataclasses.field(
24
22
  default_factory=lambda: Nothing()
25
23
  )
26
- poll: Option[telegrinder.types.Poll] = dataclasses.field(
27
- default_factory=lambda: Nothing()
28
- )
29
- video: Option[telegrinder.types.Video] = dataclasses.field(
30
- default_factory=lambda: Nothing()
31
- )
24
+ poll: Option[telegrinder.types.Poll] = dataclasses.field(default_factory=lambda: Nothing())
25
+ video: Option[telegrinder.types.Video] = dataclasses.field(default_factory=lambda: Nothing())
32
26
 
33
27
  @classmethod
34
28
  async def compose(cls, message: MessageNode) -> "Attachment":
@@ -44,31 +38,41 @@ class Photo(DataNode):
44
38
 
45
39
  @classmethod
46
40
  async def compose(cls, attachment: Attachment) -> typing.Self:
47
- return cls(attachment.photo.expect(ComposeError("Attachment is not an photo")))
41
+ if not attachment.photo:
42
+ raise ComposeError("Attachment is not an photo")
43
+ return cls(attachment.photo.unwrap())
48
44
 
49
45
 
50
46
  class Video(ScalarNode, telegrinder.types.Video):
51
47
  @classmethod
52
48
  async def compose(cls, attachment: Attachment) -> telegrinder.types.Video:
53
- return attachment.video.expect(ComposeError("Attachment is not an video"))
49
+ if not attachment.video:
50
+ raise ComposeError("Attachment is not an video")
51
+ return attachment.video.unwrap()
54
52
 
55
53
 
56
54
  class Audio(ScalarNode, telegrinder.types.Audio):
57
55
  @classmethod
58
56
  async def compose(cls, attachment: Attachment) -> telegrinder.types.Audio:
59
- return attachment.audio.expect(ComposeError("Attachment is not an audio"))
57
+ if not attachment.audio:
58
+ raise ComposeError("Attachment is not an audio")
59
+ return attachment.audio.unwrap()
60
60
 
61
61
 
62
62
  class Document(ScalarNode, telegrinder.types.Document):
63
63
  @classmethod
64
64
  async def compose(cls, attachment: Attachment) -> telegrinder.types.Document:
65
- return attachment.document.expect(ComposeError("Attachment is not an document"))
65
+ if not attachment.document:
66
+ raise ComposeError("Attachment is not an document")
67
+ return attachment.document.unwrap()
66
68
 
67
69
 
68
70
  class Poll(ScalarNode, telegrinder.types.Poll):
69
71
  @classmethod
70
72
  async def compose(cls, attachment: Attachment) -> telegrinder.types.Poll:
71
- return attachment.poll.expect(ComposeError("Attachment is not an poll"))
73
+ if not attachment.poll:
74
+ raise ComposeError("Attachment is not an poll")
75
+ return attachment.poll.unwrap()
72
76
 
73
77
 
74
78
  __all__ = (
telegrinder/node/base.py CHANGED
@@ -3,8 +3,7 @@ import inspect
3
3
  import typing
4
4
 
5
5
  ComposeResult: typing.TypeAlias = (
6
- typing.Coroutine[typing.Any, typing.Any, typing.Any]
7
- | typing.AsyncGenerator[typing.Any, None]
6
+ typing.Coroutine[typing.Any, typing.Any, typing.Any] | typing.AsyncGenerator[typing.Any, None]
8
7
  )
9
8
 
10
9
 
@@ -16,9 +15,7 @@ class Node(abc.ABC):
16
15
 
17
16
  @classmethod
18
17
  @abc.abstractmethod
19
- def compose(
20
- cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any
21
- ) -> ComposeResult:
18
+ def compose(cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any) -> ComposeResult:
22
19
  pass
23
20
 
24
21
  @classmethod
@@ -52,18 +49,14 @@ class DataNode(Node, abc.ABC):
52
49
  @typing.dataclass_transform()
53
50
  @classmethod
54
51
  @abc.abstractmethod
55
- async def compose(
56
- cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any
57
- ) -> ComposeResult:
52
+ async def compose(cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any) -> ComposeResult:
58
53
  pass
59
54
 
60
55
 
61
56
  class ScalarNodeProto(Node, abc.ABC):
62
57
  @classmethod
63
58
  @abc.abstractmethod
64
- async def compose(
65
- cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any
66
- ) -> ComposeResult:
59
+ async def compose(cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any) -> ComposeResult:
67
60
  pass
68
61
 
69
62
 
@@ -27,9 +27,7 @@ class NodeSession:
27
27
  self.generator = None
28
28
 
29
29
  def __repr__(self) -> str:
30
- return f"<{self.__class__.__name__}: {self.value}" + (
31
- "ACTIVE>" if self.generator else ">"
32
- )
30
+ return f"<{self.__class__.__name__}: {self.value}" + ("ACTIVE>" if self.generator else ">")
33
31
 
34
32
 
35
33
  class NodeCollection:
@@ -9,8 +9,10 @@ from .update import UpdateNode
9
9
  class MessageNode(ScalarNode, MessageCute):
10
10
  @classmethod
11
11
  async def compose(cls, update: UpdateNode) -> typing.Self:
12
+ if not update.message:
13
+ raise ComposeError
12
14
  return cls(
13
- **update.message.expect(ComposeError).to_dict(),
15
+ **update.message.unwrap().to_dict(),
14
16
  api=update.api,
15
17
  )
16
18
 
@@ -24,7 +24,9 @@ class Source(DataNode):
24
24
 
25
25
  async def send(self, text: str) -> Message:
26
26
  result = await self.api.send_message(
27
- self.chat.id, message_thread_id=self.thread_id, text=text
27
+ chat_id=self.chat.id,
28
+ message_thread_id=self.thread_id,
29
+ text=text,
28
30
  )
29
31
  return result.unwrap()
30
32
 
telegrinder/node/text.py CHANGED
@@ -7,7 +7,9 @@ from .message import MessageNode
7
7
  class Text(ScalarNode, str):
8
8
  @classmethod
9
9
  async def compose(cls, message: MessageNode) -> typing.Self:
10
- return cls(message.text.expect(ComposeError("Message has no text")))
10
+ if not message.text:
11
+ raise ComposeError("Message has no text")
12
+ return cls(message.text.unwrap())
11
13
 
12
14
 
13
15
  __all__ = ("Text",)
@@ -66,6 +66,7 @@ from .keyboard import (
66
66
  Keyboard,
67
67
  RowButtons,
68
68
  )
69
+ from .limited_dict import LimitedDict
69
70
  from .loop_wrapper import ABCLoopWrapper, DelayedTask, Lifespan, LoopWrapper
70
71
  from .magic import magic_bundle, resolve_arg_names
71
72
  from .parse_mode import ParseMode
@@ -100,6 +101,7 @@ __all__ = (
100
101
  "KeyboardSetYAML",
101
102
  "Lifespan",
102
103
  "Link",
104
+ "LimitedDict",
103
105
  "LoopWrapper",
104
106
  "Mention",
105
107
  "ParseMode",
@@ -63,15 +63,13 @@ class InlineButton(BaseButton):
63
63
  url: str | None = None
64
64
  login_url: dict[str, typing.Any] | LoginUrl | None = None
65
65
  pay: bool | None = None
66
- callback_data: (
67
- str | dict[str, typing.Any] | DataclassInstance | msgspec.Struct | None
68
- ) = None
66
+ callback_data: str | dict[str, typing.Any] | DataclassInstance | msgspec.Struct | None = None
69
67
  callback_game: dict[str, typing.Any] | CallbackGame | None = None
70
68
  switch_inline_query: str | None = None
71
69
  switch_inline_query_current_chat: str | None = None
72
- switch_inline_query_chosen_chat: (
73
- dict[str, typing.Any] | SwitchInlineQueryChosenChat | None
74
- ) = None
70
+ switch_inline_query_chosen_chat: dict[str, typing.Any] | SwitchInlineQueryChosenChat | None = (
71
+ None
72
+ )
75
73
  web_app: dict[str, typing.Any] | WebAppInfo | None = None
76
74
 
77
75
 
@@ -16,9 +16,7 @@ class ABCErrorHandler(ABC, typing.Generic[EventT]):
16
16
  self,
17
17
  *args: typing.Any,
18
18
  **kwargs: typing.Any,
19
- ) -> typing.Callable[
20
- [typing.Callable[..., typing.Any]], typing.Callable[..., typing.Any]
21
- ]:
19
+ ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Callable[..., typing.Any]]:
22
20
  """Decorator for registering callback as an error handler."""
23
21
 
24
22
  @abstractmethod
@@ -1,10 +1,7 @@
1
- import typing
2
-
3
-
4
- class CatcherError(TypeError):
5
- def __init__(self, exc: typing.Any, error: str) -> None:
1
+ class CatcherError(BaseException):
2
+ def __init__(self, exc: BaseException, message: str) -> None:
6
3
  self.exc = exc
7
- self.error = error
4
+ self.message = message
8
5
 
9
6
 
10
7
  __all__ = ("CatcherError",)
@@ -13,9 +13,7 @@ from .error import CatcherError
13
13
 
14
14
  F = typing.TypeVar("F", bound="FuncCatcher")
15
15
  ExceptionT = typing.TypeVar("ExceptionT", bound=BaseException, contravariant=True)
16
- FuncCatcher = typing.Callable[
17
- typing.Concatenate[ExceptionT, ...], typing.Awaitable[typing.Any]
18
- ]
16
+ FuncCatcher = typing.Callable[typing.Concatenate[ExceptionT, ...], typing.Awaitable[typing.Any]]
19
17
 
20
18
 
21
19
  @dataclasses.dataclass(frozen=True, repr=False)
@@ -59,8 +57,7 @@ class Catcher(typing.Generic[EventT]):
59
57
  ) -> Result[typing.Any, BaseException]:
60
58
  if self.match_exception(exception):
61
59
  logger.debug(
62
- "Catcher {!r} caught an exception in handler {!r}, "
63
- "running catcher...".format(
60
+ "Catcher {!r} caught an exception in handler {!r}, " "running catcher...".format(
64
61
  self.func.__name__,
65
62
  handler_name,
66
63
  )
@@ -109,7 +106,8 @@ class ErrorHandler(ABCErrorHandler[EventT]):
109
106
  ignore_errors: bool = False,
110
107
  ):
111
108
  """Register the catcher.
112
- :param logging: Error logging in stderr.
109
+
110
+ :param logging: Logging the result of the catcher at the level 'DEBUG'.
113
111
  :param raise_exception: Raise an exception if the catcher hasn't started.
114
112
  :param ignore_errors: Ignore errors that may occur.
115
113
  """
@@ -142,21 +140,21 @@ class ErrorHandler(ABCErrorHandler[EventT]):
142
140
  return Error(
143
141
  CatcherError(
144
142
  exc,
145
- "Exception {!r} was occurred during the running catcher {!r}.".format(
143
+ "Exception {} was occurred during the running catcher {!r}.".format(
146
144
  repr(exc), self.catcher.func.__name__
147
145
  ),
148
146
  )
149
147
  )
150
148
 
151
- def process_catcher_error(self, error: CatcherError) -> Result[None, str]:
149
+ def process_catcher_error(self, error: CatcherError) -> Result[None, BaseException]:
152
150
  assert self.catcher is not None
153
151
 
154
152
  if self.catcher.raise_exception:
155
153
  raise error.exc from None
156
- if not self.catcher.ignore_errors:
157
- return Error(error.error)
158
154
  if self.catcher.logging:
159
- logger.error(error.error)
155
+ logger.error(error.message)
156
+ if not self.catcher.ignore_errors:
157
+ return Error(error.exc)
160
158
 
161
159
  return Ok(None)
162
160
 
@@ -166,12 +164,18 @@ class ErrorHandler(ABCErrorHandler[EventT]):
166
164
  event: EventT,
167
165
  api: ABCAPI,
168
166
  ctx: Context,
169
- ) -> Result[typing.Any, typing.Any]:
167
+ ) -> Result[typing.Any, BaseException]:
170
168
  if not self.catcher:
171
169
  return Ok(await handler(event, **magic_bundle(handler, ctx))) # type: ignore
172
170
 
173
171
  match await self.process(handler, event, api, ctx):
174
- case Ok(_) as ok:
172
+ case Ok(value) as ok:
173
+ if self.catcher.logging:
174
+ logger.debug(
175
+ "Catcher {!r} returned a value: {!r}",
176
+ self.catcher.func.__name__,
177
+ value,
178
+ )
175
179
  return ok
176
180
  case Error(exc) as err:
177
181
  if isinstance(exc, CatcherError):
@@ -49,9 +49,7 @@ class StringFormatter(string.Formatter):
49
49
  )
50
50
  return fmt
51
51
 
52
- def get_spec_formatter(
53
- self, value: SpecialFormat
54
- ) -> typing.Callable[..., "TagFormat"]:
52
+ def get_spec_formatter(self, value: SpecialFormat) -> typing.Callable[..., "TagFormat"]:
55
53
  return globals()[value.__formatter_name__]
56
54
 
57
55
  def check_formats(self, value: typing.Any, fmts: list[str]) -> "TagFormat":
@@ -241,9 +239,7 @@ def start_bot_link(bot_id: str | int, data: str, string: str | None = None) -> T
241
239
  return link(get_start_bot_link(bot_id, data), string)
242
240
 
243
241
 
244
- def start_group_link(
245
- bot_id: str | int, data: str, string: str | None = None
246
- ) -> TagFormat:
242
+ def start_group_link(bot_id: str | int, data: str, string: str | None = None) -> TagFormat:
247
243
  return link(get_start_group_link(bot_id, data), string)
248
244
 
249
245
 
@@ -29,9 +29,7 @@ def get_invite_chat_link(invite_link: str) -> str:
29
29
 
30
30
 
31
31
  def user_open_message_link(user_id: int, message: str | None = None) -> str:
32
- return f"tg://openmessage?user_id={user_id}" + (
33
- "" if not message else f"&msg?text={message}"
34
- )
32
+ return f"tg://openmessage?user_id={user_id}" + ("" if not message else f"&msg?text={message}")
35
33
 
36
34
 
37
35
  __all__ = (