telegrinder 0.1.dev162__py3-none-any.whl → 0.1.dev164__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 (38) hide show
  1. telegrinder/bot/bot.py +7 -11
  2. telegrinder/bot/dispatch/abc.py +1 -1
  3. telegrinder/bot/dispatch/composition.py +15 -1
  4. telegrinder/bot/dispatch/context.py +8 -0
  5. telegrinder/bot/dispatch/dispatch.py +67 -31
  6. telegrinder/bot/dispatch/handler/abc.py +2 -4
  7. telegrinder/bot/dispatch/handler/func.py +31 -25
  8. telegrinder/bot/dispatch/handler/message_reply.py +21 -9
  9. telegrinder/bot/dispatch/middleware/abc.py +1 -1
  10. telegrinder/bot/dispatch/process.py +5 -3
  11. telegrinder/bot/dispatch/return_manager/callback_query.py +3 -1
  12. telegrinder/bot/dispatch/return_manager/inline_query.py +3 -1
  13. telegrinder/bot/dispatch/return_manager/message.py +9 -1
  14. telegrinder/bot/dispatch/view/abc.py +31 -5
  15. telegrinder/bot/dispatch/view/box.py +8 -7
  16. telegrinder/bot/dispatch/view/inline_query.py +1 -1
  17. telegrinder/bot/dispatch/view/message.py +1 -1
  18. telegrinder/bot/dispatch/waiter_machine/machine.py +14 -8
  19. telegrinder/bot/dispatch/waiter_machine/middleware.py +9 -7
  20. telegrinder/bot/dispatch/waiter_machine/short_state.py +17 -18
  21. telegrinder/bot/rules/__init__.py +4 -0
  22. telegrinder/bot/rules/is_from.py +18 -3
  23. telegrinder/bot/rules/text.py +1 -1
  24. telegrinder/bot/scenario/checkbox.py +16 -0
  25. telegrinder/client/aiohttp.py +1 -2
  26. telegrinder/msgspec_utils.py +51 -8
  27. telegrinder/node/base.py +1 -1
  28. telegrinder/node/composer.py +1 -1
  29. telegrinder/node/source.py +2 -3
  30. telegrinder/tools/error_handler/abc.py +3 -3
  31. telegrinder/tools/error_handler/error_handler.py +23 -23
  32. telegrinder/tools/global_context/global_context.py +7 -5
  33. telegrinder/tools/magic.py +1 -1
  34. telegrinder/types/objects.py +19 -2
  35. {telegrinder-0.1.dev162.dist-info → telegrinder-0.1.dev164.dist-info}/METADATA +1 -1
  36. {telegrinder-0.1.dev162.dist-info → telegrinder-0.1.dev164.dist-info}/RECORD +38 -38
  37. {telegrinder-0.1.dev162.dist-info → telegrinder-0.1.dev164.dist-info}/WHEEL +1 -1
  38. {telegrinder-0.1.dev162.dist-info → telegrinder-0.1.dev164.dist-info}/LICENSE +0 -0
@@ -19,12 +19,18 @@ if typing.TYPE_CHECKING:
19
19
  class WaiterMachine:
20
20
  def __init__(self) -> None:
21
21
  self.storage: Storage = {}
22
+
23
+ def __repr__(self) -> str:
24
+ return "<{}: storage={!r}>".format(
25
+ self.__class__.__name__,
26
+ self.storage,
27
+ )
22
28
 
23
29
  async def drop(
24
30
  self,
25
31
  state_view: "ABCStateView[EventModel]",
26
32
  id: Identificator,
27
- **context,
33
+ **context: typing.Any,
28
34
  ) -> None:
29
35
  view_name = state_view.__class__.__name__
30
36
  if view_name not in self.storage:
@@ -60,9 +66,9 @@ class WaiterMachine:
60
66
  *rules: ABCRule[EventModel],
61
67
  default: Behaviour = None,
62
68
  on_drop: Behaviour = None,
63
- expiration: datetime.timedelta | int | None = None,
69
+ expiration: datetime.timedelta | int | float | None = None,
64
70
  ) -> tuple[EventModel, Context]:
65
- if isinstance(expiration, int):
71
+ if isinstance(expiration, int | float):
66
72
  expiration = datetime.timedelta(seconds=expiration)
67
73
 
68
74
  api: ABCAPI
@@ -77,9 +83,9 @@ class WaiterMachine:
77
83
 
78
84
  short_state = ShortState(
79
85
  key,
80
- ctx_api=api,
81
- event=event,
82
- rules=rules,
86
+ api,
87
+ event,
88
+ rules,
83
89
  expiration=expiration,
84
90
  default_behaviour=default,
85
91
  on_drop_behaviour=on_drop,
@@ -104,13 +110,13 @@ class WaiterMachine:
104
110
  view: "ABCStateView[EventModel]",
105
111
  behaviour: Behaviour,
106
112
  event: asyncio.Event | EventModel,
107
- **context,
113
+ **context: typing.Any,
108
114
  ) -> None:
109
115
  if behaviour is None:
110
116
  return
111
117
  # TODO: add behaviour check
112
118
  # TODO: support view as a behaviour
113
- await behaviour.run(event)
119
+ await behaviour.run(event, context) # type: ignore
114
120
 
115
121
 
116
122
  __all__ = ("WaiterMachine",)
@@ -38,32 +38,34 @@ class WaiterMiddleware(ABCMiddleware[EventType]):
38
38
  if key is None:
39
39
  raise RuntimeError("Unable to get state key.")
40
40
 
41
- short_state: typing.Optional["ShortState"] = self.machine.storage[view_name].get(key)
41
+ short_state: typing.Optional["ShortState[EventType]"] = self.machine.storage[view_name].get(key)
42
42
  if not short_state:
43
43
  return True
44
44
 
45
45
  if (
46
- short_state.expiration is not None
47
- and datetime.datetime.now() >= short_state.expiration
46
+ short_state.expiration_date is not None
47
+ and datetime.datetime.now() >= short_state.expiration_date
48
48
  ):
49
49
  await self.machine.drop(self.view, short_state.key)
50
50
  return True
51
51
 
52
52
  handler = FuncHandler(
53
- self.pass_runtime, list(short_state.rules), dataclass=None
53
+ self.pass_runtime,
54
+ list(short_state.rules),
55
+ dataclass=None,
54
56
  )
55
- handler.ctx.set("short_state", short_state)
57
+ handler.preset_context.set("short_state", short_state)
56
58
  result = await handler.check(event.ctx_api, ctx.raw_update, ctx)
57
59
 
58
60
  if result is True:
59
- await handler.run(event)
61
+ await handler.run(event, ctx)
60
62
 
61
63
  elif short_state.default_behaviour is not None:
62
64
  await self.machine.call_behaviour(
63
65
  self.view,
64
66
  short_state.default_behaviour,
65
67
  event,
66
- **handler.ctx,
68
+ **handler.preset_context,
67
69
  )
68
70
 
69
71
  return False
@@ -1,8 +1,9 @@
1
1
  import asyncio
2
+ import dataclasses
2
3
  import datetime
3
4
  import typing
4
5
 
5
- from telegrinder.api.abc import ABCAPI
6
+ from telegrinder.api import ABCAPI
6
7
  from telegrinder.bot.cute_types import BaseCute
7
8
  from telegrinder.bot.dispatch.handler.abc import ABCHandler
8
9
  from telegrinder.bot.rules.abc import ABCRule
@@ -14,24 +15,22 @@ EventModel = typing.TypeVar("EventModel", bound=BaseCute)
14
15
  Behaviour: typing.TypeAlias = ABCHandler | None
15
16
 
16
17
 
18
+ @dataclasses.dataclass
17
19
  class ShortState(typing.Generic[EventModel]):
18
- def __init__(
19
- self,
20
- key: "Identificator",
21
- ctx_api: ABCAPI,
22
- event: asyncio.Event,
23
- rules: tuple[ABCRule[EventModel], ...],
24
- expiration: datetime.timedelta | None = None,
25
- default_behaviour: Behaviour | None = None,
26
- on_drop_behaviour: Behaviour | None = None,
27
- ) -> None:
28
- self.key = key
29
- self.ctx_api = ctx_api
30
- self.event = event
31
- self.rules = rules
32
- self.default_behaviour = default_behaviour
33
- self.expiration = (datetime.datetime.now() + expiration) if expiration else None
34
- self.on_drop_behaviour = on_drop_behaviour
20
+ key: "Identificator"
21
+ ctx_api: ABCAPI
22
+ event: asyncio.Event
23
+ rules: tuple[ABCRule[EventModel], ...]
24
+ _: dataclasses.KW_ONLY
25
+ expiration: dataclasses.InitVar[datetime.timedelta | None] = dataclasses.field(default=None)
26
+ default_behaviour: Behaviour | None = dataclasses.field(default=None)
27
+ on_drop_behaviour: Behaviour | None = dataclasses.field(default=None)
28
+ expiration_date: datetime.datetime | None = dataclasses.field(init=False)
29
+
30
+ def __post_init__(self, expiration: datetime.timedelta | None = None) -> None:
31
+ self.expiration_date = (
32
+ datetime.datetime.now() - expiration
33
+ ) if expiration is not None else None
35
34
 
36
35
 
37
36
  __all__ = ("ShortState",)
@@ -30,6 +30,8 @@ from .is_from import (
30
30
  IsDartDice,
31
31
  IsDice,
32
32
  IsForum,
33
+ IsForward,
34
+ IsForwardType,
33
35
  IsGroup,
34
36
  IsLanguageCode,
35
37
  IsPremium,
@@ -78,6 +80,8 @@ __all__ = (
78
80
  "IsDartDice",
79
81
  "IsDice",
80
82
  "IsForum",
83
+ "IsForward",
84
+ "IsForwardType",
81
85
  "IsGroup",
82
86
  "IsLanguageCode",
83
87
  "IsPremium",
@@ -12,13 +12,13 @@ T = typing.TypeVar("T", bound=BaseCute)
12
12
 
13
13
 
14
14
  @typing.runtime_checkable
15
- class HasFromProto(typing.Protocol):
15
+ class FromUserProto(typing.Protocol):
16
16
  from_: User | Option[User]
17
17
 
18
18
 
19
19
  class HasFrom(ABCRule[T]):
20
20
  async def check(self, event: T, ctx: Context) -> bool:
21
- return isinstance(event, HasFromProto) and bool(event.from_)
21
+ return isinstance(event, FromUserProto) and bool(event.from_)
22
22
 
23
23
 
24
24
  class HasDice(MessageRule):
@@ -26,6 +26,19 @@ class HasDice(MessageRule):
26
26
  return bool(message.dice)
27
27
 
28
28
 
29
+ class IsForward(MessageRule):
30
+ async def check(self, message: Message, ctx: Context) -> bool:
31
+ return bool(message.forward_origin)
32
+
33
+
34
+ class IsForwardType(MessageRule, requires=[IsForward()]):
35
+ def __init__(self, fwd_type: typing.Literal["user", "hidden_user", "chat", "channel"], /) -> None:
36
+ self.fwd_type = fwd_type
37
+
38
+ async def check(self, message: Message, ctx: Context) -> bool:
39
+ return message.forward_origin.unwrap().v.type == self.fwd_type
40
+
41
+
29
42
  class IsReply(MessageRule):
30
43
  async def check(self, message: Message, ctx: Context) -> bool:
31
44
  return bool(message.reply_to_message)
@@ -58,7 +71,7 @@ class IsLanguageCode(MessageRule, requires=[HasFrom()]):
58
71
  async def check(self, message: Message, ctx: Context) -> bool:
59
72
  if not message.from_user.language_code:
60
73
  return False
61
- return message.from_user.language_code.unwrap_or_none() in self.lang_codes
74
+ return message.from_user.language_code.unwrap() in self.lang_codes
62
75
 
63
76
 
64
77
  class IsForum(MessageRule):
@@ -141,6 +154,8 @@ __all__ = (
141
154
  "IsDartDice",
142
155
  "IsDice",
143
156
  "IsForum",
157
+ "IsForward",
158
+ "IsForwardType",
144
159
  "IsGroup",
145
160
  "IsLanguageCode",
146
161
  "IsPremium",
@@ -14,7 +14,7 @@ class TextMessageRule(MessageRule, ABC, requires=[HasText()]):
14
14
 
15
15
 
16
16
  class Text(TextMessageRule):
17
- def __init__(self, texts: str | list[str], ignore_case: bool = False):
17
+ def __init__(self, texts: str | list[str], *, ignore_case: bool = False) -> None:
18
18
  if not isinstance(texts, list):
19
19
  texts = [texts]
20
20
  self.texts = texts if not ignore_case else list(map(str.lower, texts))
@@ -45,6 +45,21 @@ class Checkbox(ABCScenario[CallbackQueryCute]):
45
45
  self.max_in_row = max_in_row
46
46
  self.random_code = secrets.token_hex(8)
47
47
  self.waiter_machine = waiter_machine
48
+
49
+ def __repr__(self) -> str:
50
+ return (
51
+ "<{}@{!r}: (choices={!r}, max_in_row={}) with waiter_machine={!r}, ready_text={!r} "
52
+ "for chat_id={} with message={!r}>"
53
+ ).format(
54
+ self.__class__.__name__,
55
+ self.random_code,
56
+ self.choices,
57
+ self.max_in_row,
58
+ self.waiter_machine,
59
+ self.ready,
60
+ self.chat_id,
61
+ self.msg,
62
+ )
48
63
 
49
64
  def get_markup(self) -> InlineKeyboardMarkup:
50
65
  kb = InlineKeyboard()
@@ -70,6 +85,7 @@ class Checkbox(ABCScenario[CallbackQueryCute]):
70
85
  name: str,
71
86
  default_text: str,
72
87
  picked_text: str,
88
+ *,
73
89
  is_picked: bool = False,
74
90
  ) -> typing.Self:
75
91
  self.choices.append(
@@ -1,4 +1,3 @@
1
- import secrets
2
1
  import ssl
3
2
  import typing
4
3
 
@@ -31,7 +30,7 @@ class AiohttpClient(ABCClient):
31
30
  self.__class__.__name__,
32
31
  self.session,
33
32
  self.timeout,
34
- False if self.session is None else self.session.closed,
33
+ True if self.session is None else self.session.closed,
35
34
  )
36
35
 
37
36
  async def request_raw(
@@ -4,9 +4,6 @@ import fntypes.option
4
4
  import msgspec
5
5
  from fntypes.co import Error, Ok, Result, Variative
6
6
 
7
- T = typing.TypeVar("T")
8
- Ts = typing.TypeVarTuple("Ts")
9
-
10
7
  if typing.TYPE_CHECKING:
11
8
  from datetime import datetime
12
9
 
@@ -18,6 +15,7 @@ else:
18
15
 
19
16
  datetime = type("datetime", (dt,), {})
20
17
 
18
+
21
19
  class OptionMeta(type):
22
20
  def __instancecheck__(cls, __instance: typing.Any) -> bool:
23
21
  return isinstance(__instance, fntypes.option.Some | fntypes.option.Nothing)
@@ -26,6 +24,9 @@ else:
26
24
  class Option(typing.Generic[Value], metaclass=OptionMeta):
27
25
  pass
28
26
 
27
+ T = typing.TypeVar("T")
28
+ Ts = typing.TypeVarTuple("Ts")
29
+
29
30
  DecHook: typing.TypeAlias = typing.Callable[[type[T], typing.Any], object]
30
31
  EncHook: typing.TypeAlias = typing.Callable[[T], typing.Any]
31
32
 
@@ -90,6 +91,32 @@ def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
90
91
 
91
92
 
92
93
  class Decoder:
94
+ """Class `Decoder` for `msgspec` module with decode hook
95
+ for objects with the specified type.
96
+
97
+ ```
98
+ import enum
99
+
100
+ from datetime import datetime as dt
101
+
102
+ class Digit(enum.IntEnum):
103
+ ONE = 1
104
+ TWO = 2
105
+ THREE = 3
106
+
107
+ decoder = Encoder()
108
+ decoder.dec_hooks[dt] = lambda t, timestamp: t.fromtimestamp(timestamp)
109
+
110
+ decoder.dec_hook(dt, 1713354732) #> datetime.datetime(2024, 4, 17, 14, 52, 12)
111
+ decoder.dec_hook(int, "123") #> TypeError: Unknown type `int`. You can implement decode hook for this type.
112
+
113
+ decoder.convert("123", type=int, strict=False) #> 123
114
+ decoder.convert(1, type=Digit) #> <Digit.ONE: 1>
115
+
116
+ decoder.decode(b'{"digit":3}', type=dict[str, Digit]) #> {'digit': <Digit.THREE: 3>}
117
+ ```
118
+ """
119
+
93
120
  def __init__(self) -> None:
94
121
  self.dec_hooks: dict[typing.Any, DecHook[typing.Any]] = {
95
122
  Option: option_dec_hook,
@@ -119,7 +146,7 @@ class Decoder:
119
146
  type: type[T] = dict,
120
147
  strict: bool = True,
121
148
  from_attributes: bool = False,
122
- builtin_types: typing.Iterable[type] | None = None,
149
+ builtin_types: typing.Iterable[type[typing.Any]] | None = None,
123
150
  str_keys: bool = False,
124
151
  ) -> T:
125
152
  return msgspec.convert(
@@ -166,6 +193,21 @@ class Decoder:
166
193
 
167
194
 
168
195
  class Encoder:
196
+ """Class `Encoder` for `msgspec` module with encode hooks for objects.
197
+
198
+ ```
199
+ from datetime import datetime as dt
200
+
201
+ encoder = Encoder()
202
+ encoder.enc_hooks[dt] = lambda d: int(d.timestamp())
203
+
204
+ encoder.enc_hook(dt.now()) #> 1713354732
205
+ encoder.enc_hook(123) #> NotImplementedError: Not implemented encode hook for object of type `int`.
206
+
207
+ encoder.encode({'digit': Digit.ONE}) #> '{"digit":1}'
208
+ ```
209
+ """
210
+
169
211
  def __init__(self) -> None:
170
212
  self.enc_hooks: dict[typing.Any, EncHook[typing.Any]] = {
171
213
  fntypes.option.Some: lambda opt: opt.value,
@@ -174,9 +216,10 @@ class Encoder:
174
216
  datetime: lambda date: int(date.timestamp()),
175
217
  }
176
218
 
177
- def add_dec_hook(self, tp: type[T]):
219
+ def add_dec_hook(self, t: type[T]):
178
220
  def decorator(func: EncHook[T]) -> EncHook[T]:
179
- return self.enc_hooks.setdefault(get_origin(tp), func)
221
+ encode_hook = self.enc_hooks.setdefault(get_origin(t), func)
222
+ return func if encode_hook is not func else encode_hook
180
223
 
181
224
  return decorator
182
225
 
@@ -194,11 +237,11 @@ class Encoder:
194
237
  ...
195
238
 
196
239
  @typing.overload
197
- def encode(self, obj: typing.Any, *, as_str: typing.Literal[True] = True) -> str:
240
+ def encode(self, obj: typing.Any, *, as_str: typing.Literal[True]) -> str:
198
241
  ...
199
242
 
200
243
  @typing.overload
201
- def encode(self, obj: typing.Any, *, as_str: typing.Literal[False] = False) -> bytes:
244
+ def encode(self, obj: typing.Any, *, as_str: typing.Literal[False]) -> bytes:
202
245
  ...
203
246
 
204
247
  def encode(self, obj: typing.Any, *, as_str: bool = True) -> str | bytes:
telegrinder/node/base.py CHANGED
@@ -6,7 +6,7 @@ ComposeResult: typing.TypeAlias = typing.Coroutine[typing.Any, typing.Any, typin
6
6
 
7
7
 
8
8
  class ComposeError(BaseException):
9
- pass
9
+ ...
10
10
 
11
11
 
12
12
  class Node(abc.ABC):
@@ -27,7 +27,7 @@ class NodeSession:
27
27
  self.generator = None
28
28
 
29
29
  def __repr__(self) -> str:
30
- return f"<NodeSession {self.value}" + ("ACTIVE>" if self.generator else ">")
30
+ return f"<NodeSession: {self.value}" + ("ACTIVE>" if self.generator else ">")
31
31
 
32
32
 
33
33
  class NodeCollection:
@@ -2,7 +2,6 @@ import dataclasses
2
2
  import typing
3
3
 
4
4
  from telegrinder.api import API
5
- from telegrinder.msgspec_utils import Nothing, Option
6
5
  from telegrinder.types import Chat, Message
7
6
 
8
7
  from .base import DataNode
@@ -13,14 +12,14 @@ from .message import MessageNode
13
12
  class Source(DataNode):
14
13
  api: API
15
14
  chat: Chat
16
- thread_id: Option[int] = dataclasses.field(default_factory=lambda: Nothing)
15
+ thread_id: int | None = None
17
16
 
18
17
  @classmethod
19
18
  async def compose(cls, message: MessageNode) -> typing.Self:
20
19
  return cls(
21
20
  api=message.ctx_api,
22
21
  chat=message.chat,
23
- thread_id=message.message_thread_id,
22
+ thread_id=message.message_thread_id.unwrap_or_none(),
24
23
  )
25
24
 
26
25
  async def send(self, text: str) -> Message:
@@ -13,12 +13,12 @@ Handler = typing.Callable[typing.Concatenate[EventT, ...], typing.Awaitable[typi
13
13
 
14
14
  class ABCErrorHandler(ABC, typing.Generic[EventT]):
15
15
  @abstractmethod
16
- def register_catcher(
16
+ def __call__(
17
17
  self,
18
18
  *args: typing.Any,
19
19
  **kwargs: typing.Any,
20
20
  ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Callable[..., typing.Any]]:
21
- ...
21
+ """Decorator for registering callback as an error handler."""
22
22
 
23
23
  @abstractmethod
24
24
  async def run(
@@ -28,7 +28,7 @@ class ABCErrorHandler(ABC, typing.Generic[EventT]):
28
28
  api: ABCAPI,
29
29
  ctx: Context,
30
30
  ) -> Result[typing.Any, typing.Any]:
31
- ...
31
+ """Run error handler."""
32
32
 
33
33
 
34
34
  __all__ = ("ABCErrorHandler",)
@@ -74,9 +74,9 @@ class Catcher(typing.Generic[EventT]):
74
74
 
75
75
  def match_exception(self, exception: BaseException) -> bool:
76
76
  for exc in self.exceptions:
77
- if isinstance(exc, type) and type(exception) == exc:
77
+ if isinstance(exc, type) and type(exception) is exc:
78
78
  return True
79
- if isinstance(exc, object) and type(exception) == type(exc):
79
+ if isinstance(exc, object) and type(exception) is type(exc):
80
80
  return True if not exc.args else exc.args == exception.args
81
81
  return False
82
82
 
@@ -97,27 +97,8 @@ class ErrorHandler(ABCErrorHandler[EventT]):
97
97
  if self.catcher is not None
98
98
  else "<ErrorHandler: No catcher>"
99
99
  )
100
-
101
- async def __call__(
102
- self,
103
- handler: Handler[EventT],
104
- event: EventT,
105
- api: ABCAPI,
106
- ctx: Context,
107
- ) -> Result[typing.Any, BaseException]:
108
- assert self.catcher is not None
109
100
 
110
- try:
111
- return await self.catcher(handler, event, api, ctx)
112
- except BaseException as exc:
113
- return Error(CatcherError(
114
- exc,
115
- "Exception {!r} was occurred during the running catcher {!r}.".format(
116
- repr(exc), self.catcher.func.__name__
117
- )
118
- ))
119
-
120
- def register_catcher(
101
+ def __call__(
121
102
  self,
122
103
  *exceptions: type[BaseException] | BaseException,
123
104
  logging: bool = False,
@@ -142,6 +123,25 @@ class ErrorHandler(ABCErrorHandler[EventT]):
142
123
  return func
143
124
  return decorator
144
125
 
126
+ async def process(
127
+ self,
128
+ handler: Handler[EventT],
129
+ event: EventT,
130
+ api: ABCAPI,
131
+ ctx: Context,
132
+ ) -> Result[typing.Any, BaseException]:
133
+ assert self.catcher is not None
134
+
135
+ try:
136
+ return await self.catcher(handler, event, api, ctx)
137
+ except BaseException as exc:
138
+ return Error(CatcherError(
139
+ exc,
140
+ "Exception {!r} was occurred during the running catcher {!r}.".format(
141
+ repr(exc), self.catcher.func.__name__
142
+ )
143
+ ))
144
+
145
145
  def process_catcher_error(self, error: CatcherError) -> Result[None, str]:
146
146
  assert self.catcher is not None
147
147
 
@@ -164,7 +164,7 @@ class ErrorHandler(ABCErrorHandler[EventT]):
164
164
  if not self.catcher:
165
165
  return Ok(await handler(event, **magic_bundle(handler, ctx))) # type: ignore
166
166
 
167
- match await self(handler, event, api, ctx):
167
+ match await self.process(handler, event, api, ctx):
168
168
  case Ok(_) as ok:
169
169
  return ok
170
170
  case Error(exc) as err:
@@ -23,11 +23,13 @@ else:
23
23
 
24
24
 
25
25
  def type_check(value: object, value_type: type[T]) -> typing.TypeGuard[T]:
26
- return (
27
- True
28
- if value_type in (typing.Any, object)
29
- else bool(msgspec_convert(value, value_type))
30
- )
26
+ if value_type in (typing.Any, object):
27
+ return True
28
+ match msgspec_convert(value, value_type):
29
+ case Ok(v):
30
+ return type(value) is type(v)
31
+ case Error(_):
32
+ return False
31
33
 
32
34
 
33
35
  def is_dunder(name: str) -> bool:
@@ -9,7 +9,7 @@ if typing.TYPE_CHECKING:
9
9
  T = typing.TypeVar("T", bound=ABCRule)
10
10
 
11
11
  FuncType: typing.TypeAlias = types.FunctionType | typing.Callable[..., typing.Any]
12
- TRANSLATIONS_KEY = "_translations"
12
+ TRANSLATIONS_KEY: typing.Final[str] = "_translations"
13
13
 
14
14
 
15
15
  def resolve_arg_names(func: FuncType, start_idx: int = 1) -> tuple[str, ...]:
@@ -2864,6 +2864,23 @@ class Birthdate(Model):
2864
2864
  year: Option[int] = Nothing
2865
2865
  """Optional. Year of the user's birth."""
2866
2866
 
2867
+ @property
2868
+ def is_birthday(self) -> bool:
2869
+ """True, if today is a user's birthday."""
2870
+
2871
+ now = datetime.now()
2872
+ return now.month == self.month and now.day == self.day
2873
+
2874
+ @property
2875
+ def age(self) -> Option[int]:
2876
+ """Optional. Contains the user's age, if the user has a birth year specified."""
2877
+
2878
+ return self.year.map(
2879
+ lambda year: (
2880
+ (datetime.now() - datetime(year, self.month, self.day)) // 365
2881
+ ).days
2882
+ )
2883
+
2867
2884
 
2868
2885
  class BusinessIntro(Model):
2869
2886
  """Object `BusinessIntro`, see the [documentation](https://core.telegram.org/bots/api#businessintro).
@@ -2902,11 +2919,11 @@ class BusinessOpeningHoursInterval(Model):
2902
2919
 
2903
2920
  opening_minute: int
2904
2921
  """The minute's sequence number in a week, starting on Monday, marking the
2905
- start of the time interval during which the business is open; 0 - 7 24 60."""
2922
+ start of the time interval during which the business is open; 0 - 7 * 24 * 60."""
2906
2923
 
2907
2924
  closing_minute: int
2908
2925
  """The minute's sequence number in a week, starting on Monday, marking the
2909
- end of the time interval during which the business is open; 0 - 8 24 60."""
2926
+ end of the time interval during which the business is open; 0 - 8 * 24 * 60."""
2910
2927
 
2911
2928
 
2912
2929
  class BusinessOpeningHours(Model):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: telegrinder
3
- Version: 0.1.dev162
3
+ Version: 0.1.dev164
4
4
  Summary: Framework for effective and reliable async telegram bot building.
5
5
  Home-page: https://github.com/timoniq/telegrinder
6
6
  License: MIT