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.
- telegrinder/__init__.py +45 -12
- telegrinder/bot/__init__.py +32 -4
- telegrinder/bot/cute_types/callback_query.py +60 -146
- telegrinder/bot/cute_types/chat_join_request.py +12 -16
- telegrinder/bot/cute_types/chat_member_updated.py +15 -105
- telegrinder/bot/cute_types/inline_query.py +5 -14
- telegrinder/bot/cute_types/message.py +623 -1238
- telegrinder/bot/dispatch/__init__.py +40 -3
- telegrinder/bot/dispatch/abc.py +8 -1
- telegrinder/bot/dispatch/context.py +2 -2
- telegrinder/bot/dispatch/dispatch.py +8 -1
- telegrinder/bot/dispatch/handler/__init__.py +17 -1
- telegrinder/bot/dispatch/handler/audio_reply.py +44 -0
- telegrinder/bot/dispatch/handler/base.py +57 -0
- telegrinder/bot/dispatch/handler/document_reply.py +44 -0
- telegrinder/bot/dispatch/handler/func.py +3 -3
- telegrinder/bot/dispatch/handler/media_group_reply.py +43 -0
- telegrinder/bot/dispatch/handler/message_reply.py +12 -35
- telegrinder/bot/dispatch/handler/photo_reply.py +44 -0
- telegrinder/bot/dispatch/handler/sticker_reply.py +37 -0
- telegrinder/bot/dispatch/handler/video_reply.py +44 -0
- telegrinder/bot/dispatch/process.py +2 -2
- telegrinder/bot/dispatch/return_manager/abc.py +11 -8
- telegrinder/bot/dispatch/return_manager/callback_query.py +2 -2
- telegrinder/bot/dispatch/return_manager/inline_query.py +2 -2
- telegrinder/bot/dispatch/return_manager/message.py +3 -3
- telegrinder/bot/dispatch/view/__init__.py +2 -1
- telegrinder/bot/dispatch/view/abc.py +2 -181
- telegrinder/bot/dispatch/view/base.py +200 -0
- telegrinder/bot/dispatch/view/callback_query.py +3 -3
- telegrinder/bot/dispatch/view/chat_join_request.py +2 -2
- telegrinder/bot/dispatch/view/chat_member.py +2 -3
- telegrinder/bot/dispatch/view/inline_query.py +2 -2
- telegrinder/bot/dispatch/view/message.py +5 -4
- telegrinder/bot/dispatch/view/raw.py +4 -3
- telegrinder/bot/dispatch/waiter_machine/__init__.py +18 -0
- telegrinder/bot/dispatch/waiter_machine/actions.py +10 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +15 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +60 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +49 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/message.py +54 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/state.py +19 -0
- telegrinder/bot/dispatch/waiter_machine/machine.py +88 -101
- telegrinder/bot/dispatch/waiter_machine/middleware.py +23 -41
- telegrinder/bot/dispatch/waiter_machine/short_state.py +9 -9
- telegrinder/bot/polling/polling.py +5 -2
- telegrinder/bot/rules/__init__.py +3 -3
- telegrinder/bot/rules/abc.py +6 -5
- telegrinder/bot/rules/adapter/__init__.py +1 -1
- telegrinder/bot/rules/integer.py +1 -1
- telegrinder/bot/rules/is_from.py +19 -0
- telegrinder/bot/rules/state.py +9 -6
- telegrinder/bot/scenario/checkbox.py +5 -5
- telegrinder/bot/scenario/choice.py +2 -2
- telegrinder/client/aiohttp.py +5 -7
- telegrinder/model.py +6 -11
- telegrinder/modules.py +16 -25
- telegrinder/msgspec_json.py +1 -1
- telegrinder/msgspec_utils.py +56 -5
- telegrinder/node/base.py +2 -2
- telegrinder/node/composer.py +5 -9
- telegrinder/node/container.py +6 -1
- telegrinder/node/event.py +2 -0
- telegrinder/node/polymorphic.py +7 -7
- telegrinder/node/rule.py +6 -4
- telegrinder/node/scope.py +3 -3
- telegrinder/node/source.py +4 -2
- telegrinder/node/tools/generator.py +7 -6
- telegrinder/rules.py +2 -2
- telegrinder/tools/__init__.py +10 -10
- telegrinder/tools/functional.py +9 -0
- telegrinder/tools/keyboard.py +6 -1
- telegrinder/tools/loop_wrapper/loop_wrapper.py +4 -5
- telegrinder/tools/magic.py +17 -19
- telegrinder/tools/state_storage/__init__.py +3 -3
- telegrinder/tools/state_storage/abc.py +12 -10
- telegrinder/tools/state_storage/memory.py +9 -6
- telegrinder/types/__init__.py +1 -0
- telegrinder/types/methods.py +10 -2
- telegrinder/types/objects.py +47 -5
- {telegrinder-0.2.1.dist-info → telegrinder-0.3.0.dist-info}/METADATA +4 -5
- telegrinder-0.3.0.dist-info/RECORD +164 -0
- telegrinder-0.2.1.dist-info/RECORD +0 -149
- {telegrinder-0.2.1.dist-info → telegrinder-0.3.0.dist-info}/LICENSE +0 -0
- {telegrinder-0.2.1.dist-info → telegrinder-0.3.0.dist-info}/WHEEL +0 -0
telegrinder/bot/rules/is_from.py
CHANGED
|
@@ -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
|
)
|
telegrinder/bot/rules/state.py
CHANGED
|
@@ -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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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.
|
|
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,
|
|
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.
|
|
8
|
+
from telegrinder.bot.dispatch.view.base import BaseStateView
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class Choice(Checkbox):
|
telegrinder/client/aiohttp.py
CHANGED
|
@@ -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=
|
|
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="
|
|
69
|
-
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="
|
|
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":
|
|
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:
|
|
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:
|
|
50
|
-
) -> Result[
|
|
51
|
-
return result.map(lambda v: decoder.decode(v, type=full_t))
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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__ = ("
|
|
237
|
+
__all__ = ("LoggerModule", "logger")
|
telegrinder/msgspec_json.py
CHANGED
telegrinder/msgspec_utils.py
CHANGED
|
@@ -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
|
-
|
|
68
|
-
|
|
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:
|
|
16
|
-
maybe_node = typing.get_origin(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)
|
telegrinder/node/composer.py
CHANGED
|
@@ -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))
|
|
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")
|
telegrinder/node/container.py
CHANGED
|
@@ -12,7 +12,12 @@ class ContainerNode(Node):
|
|
|
12
12
|
|
|
13
13
|
@classmethod
|
|
14
14
|
def get_subnodes(cls) -> dict[str, type[Node]]:
|
|
15
|
-
|
|
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
telegrinder/node/polymorphic.py
CHANGED
|
@@ -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,
|
|
21
|
-
logger.debug("Checking impl {}",
|
|
22
|
-
composition = Composition(
|
|
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",
|
|
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",
|
|
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 {}",
|
|
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
|
-
|
|
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
telegrinder/node/source.py
CHANGED
|
@@ -39,7 +39,9 @@ class Source(Polymorphic, DataNode):
|
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
@impl
|
|
42
|
-
async def compose_chat_join_request(
|
|
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__ = ("
|
|
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[
|
|
24
|
-
func: typing.Callable[...,
|
|
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[
|
|
27
|
-
async def compose(**kw
|
|
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
|
-
|
|
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
|
)
|