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.
- telegrinder/bot/bot.py +7 -11
- telegrinder/bot/dispatch/abc.py +1 -1
- telegrinder/bot/dispatch/composition.py +15 -1
- telegrinder/bot/dispatch/context.py +8 -0
- telegrinder/bot/dispatch/dispatch.py +67 -31
- telegrinder/bot/dispatch/handler/abc.py +2 -4
- telegrinder/bot/dispatch/handler/func.py +31 -25
- telegrinder/bot/dispatch/handler/message_reply.py +21 -9
- telegrinder/bot/dispatch/middleware/abc.py +1 -1
- telegrinder/bot/dispatch/process.py +5 -3
- telegrinder/bot/dispatch/return_manager/callback_query.py +3 -1
- telegrinder/bot/dispatch/return_manager/inline_query.py +3 -1
- telegrinder/bot/dispatch/return_manager/message.py +9 -1
- telegrinder/bot/dispatch/view/abc.py +31 -5
- telegrinder/bot/dispatch/view/box.py +8 -7
- telegrinder/bot/dispatch/view/inline_query.py +1 -1
- telegrinder/bot/dispatch/view/message.py +1 -1
- telegrinder/bot/dispatch/waiter_machine/machine.py +14 -8
- telegrinder/bot/dispatch/waiter_machine/middleware.py +9 -7
- telegrinder/bot/dispatch/waiter_machine/short_state.py +17 -18
- telegrinder/bot/rules/__init__.py +4 -0
- telegrinder/bot/rules/is_from.py +18 -3
- telegrinder/bot/rules/text.py +1 -1
- telegrinder/bot/scenario/checkbox.py +16 -0
- telegrinder/client/aiohttp.py +1 -2
- telegrinder/msgspec_utils.py +51 -8
- telegrinder/node/base.py +1 -1
- telegrinder/node/composer.py +1 -1
- telegrinder/node/source.py +2 -3
- telegrinder/tools/error_handler/abc.py +3 -3
- telegrinder/tools/error_handler/error_handler.py +23 -23
- telegrinder/tools/global_context/global_context.py +7 -5
- telegrinder/tools/magic.py +1 -1
- telegrinder/types/objects.py +19 -2
- {telegrinder-0.1.dev162.dist-info → telegrinder-0.1.dev164.dist-info}/METADATA +1 -1
- {telegrinder-0.1.dev162.dist-info → telegrinder-0.1.dev164.dist-info}/RECORD +38 -38
- {telegrinder-0.1.dev162.dist-info → telegrinder-0.1.dev164.dist-info}/WHEEL +1 -1
- {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
|
-
|
|
81
|
-
event
|
|
82
|
-
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.
|
|
47
|
-
and datetime.datetime.now() >= short_state.
|
|
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,
|
|
53
|
+
self.pass_runtime,
|
|
54
|
+
list(short_state.rules),
|
|
55
|
+
dataclass=None,
|
|
54
56
|
)
|
|
55
|
-
handler.
|
|
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.
|
|
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
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
self.
|
|
30
|
-
|
|
31
|
-
|
|
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",
|
telegrinder/bot/rules/is_from.py
CHANGED
|
@@ -12,13 +12,13 @@ T = typing.TypeVar("T", bound=BaseCute)
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@typing.runtime_checkable
|
|
15
|
-
class
|
|
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,
|
|
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.
|
|
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",
|
telegrinder/bot/rules/text.py
CHANGED
|
@@ -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(
|
telegrinder/client/aiohttp.py
CHANGED
|
@@ -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
|
-
|
|
33
|
+
True if self.session is None else self.session.closed,
|
|
35
34
|
)
|
|
36
35
|
|
|
37
36
|
async def request_raw(
|
telegrinder/msgspec_utils.py
CHANGED
|
@@ -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,
|
|
219
|
+
def add_dec_hook(self, t: type[T]):
|
|
178
220
|
def decorator(func: EncHook[T]) -> EncHook[T]:
|
|
179
|
-
|
|
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]
|
|
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]
|
|
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
telegrinder/node/composer.py
CHANGED
|
@@ -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:
|
telegrinder/node/source.py
CHANGED
|
@@ -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:
|
|
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
|
|
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)
|
|
77
|
+
if isinstance(exc, type) and type(exception) is exc:
|
|
78
78
|
return True
|
|
79
|
-
if isinstance(exc, object) and type(exception)
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
True
|
|
28
|
-
|
|
29
|
-
|
|
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:
|
telegrinder/tools/magic.py
CHANGED
|
@@ -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, ...]:
|
telegrinder/types/objects.py
CHANGED
|
@@ -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):
|