telegrinder 0.4.2__py3-none-any.whl → 0.5.1__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 +37 -55
- telegrinder/__meta__.py +1 -0
- telegrinder/api/__init__.py +6 -4
- telegrinder/api/api.py +100 -26
- telegrinder/api/error.py +42 -8
- telegrinder/api/response.py +4 -1
- telegrinder/api/token.py +2 -2
- telegrinder/bot/__init__.py +9 -25
- telegrinder/bot/bot.py +31 -25
- telegrinder/bot/cute_types/__init__.py +0 -0
- telegrinder/bot/cute_types/base.py +103 -61
- telegrinder/bot/cute_types/callback_query.py +447 -400
- telegrinder/bot/cute_types/chat_join_request.py +59 -62
- telegrinder/bot/cute_types/chat_member_updated.py +154 -157
- telegrinder/bot/cute_types/inline_query.py +41 -44
- telegrinder/bot/cute_types/message.py +98 -67
- telegrinder/bot/cute_types/pre_checkout_query.py +38 -42
- telegrinder/bot/cute_types/update.py +1 -8
- telegrinder/bot/cute_types/utils.py +1 -1
- telegrinder/bot/dispatch/__init__.py +10 -15
- telegrinder/bot/dispatch/abc.py +12 -11
- telegrinder/bot/dispatch/action.py +104 -0
- telegrinder/bot/dispatch/context.py +32 -26
- telegrinder/bot/dispatch/dispatch.py +61 -134
- telegrinder/bot/dispatch/handler/__init__.py +2 -0
- telegrinder/bot/dispatch/handler/abc.py +10 -8
- telegrinder/bot/dispatch/handler/audio_reply.py +2 -3
- telegrinder/bot/dispatch/handler/base.py +10 -33
- telegrinder/bot/dispatch/handler/document_reply.py +2 -3
- telegrinder/bot/dispatch/handler/func.py +55 -87
- telegrinder/bot/dispatch/handler/media_group_reply.py +2 -3
- telegrinder/bot/dispatch/handler/message_reply.py +2 -3
- telegrinder/bot/dispatch/handler/photo_reply.py +2 -3
- telegrinder/bot/dispatch/handler/sticker_reply.py +2 -3
- telegrinder/bot/dispatch/handler/video_reply.py +2 -3
- telegrinder/bot/dispatch/middleware/__init__.py +0 -0
- telegrinder/bot/dispatch/middleware/abc.py +79 -55
- telegrinder/bot/dispatch/middleware/global_middleware.py +18 -33
- telegrinder/bot/dispatch/process.py +84 -105
- telegrinder/bot/dispatch/return_manager/__init__.py +0 -0
- telegrinder/bot/dispatch/return_manager/abc.py +102 -65
- telegrinder/bot/dispatch/return_manager/callback_query.py +4 -5
- telegrinder/bot/dispatch/return_manager/inline_query.py +3 -4
- telegrinder/bot/dispatch/return_manager/message.py +8 -10
- telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +4 -5
- telegrinder/bot/dispatch/view/__init__.py +4 -4
- telegrinder/bot/dispatch/view/abc.py +6 -16
- telegrinder/bot/dispatch/view/base.py +54 -178
- telegrinder/bot/dispatch/view/box.py +19 -18
- telegrinder/bot/dispatch/view/callback_query.py +4 -8
- telegrinder/bot/dispatch/view/chat_join_request.py +5 -6
- telegrinder/bot/dispatch/view/chat_member.py +5 -25
- telegrinder/bot/dispatch/view/error.py +9 -0
- telegrinder/bot/dispatch/view/inline_query.py +4 -8
- telegrinder/bot/dispatch/view/message.py +5 -25
- telegrinder/bot/dispatch/view/pre_checkout_query.py +4 -8
- telegrinder/bot/dispatch/view/raw.py +3 -109
- telegrinder/bot/dispatch/waiter_machine/__init__.py +2 -5
- telegrinder/bot/dispatch/waiter_machine/actions.py +6 -4
- telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +1 -3
- telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +1 -1
- telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +11 -7
- telegrinder/bot/dispatch/waiter_machine/hasher/message.py +0 -0
- telegrinder/bot/dispatch/waiter_machine/machine.py +43 -60
- telegrinder/bot/dispatch/waiter_machine/middleware.py +19 -23
- telegrinder/bot/dispatch/waiter_machine/short_state.py +6 -5
- telegrinder/bot/polling/__init__.py +0 -0
- telegrinder/bot/polling/abc.py +0 -0
- telegrinder/bot/polling/polling.py +209 -88
- telegrinder/bot/rules/__init__.py +3 -16
- telegrinder/bot/rules/abc.py +42 -122
- telegrinder/bot/rules/callback_data.py +29 -49
- telegrinder/bot/rules/chat_join.py +5 -23
- telegrinder/bot/rules/command.py +8 -4
- telegrinder/bot/rules/enum_text.py +3 -4
- telegrinder/bot/rules/func.py +7 -14
- telegrinder/bot/rules/fuzzy.py +3 -4
- telegrinder/bot/rules/inline.py +8 -20
- telegrinder/bot/rules/integer.py +2 -3
- telegrinder/bot/rules/is_from.py +12 -11
- telegrinder/bot/rules/logic.py +11 -5
- telegrinder/bot/rules/markup.py +22 -14
- telegrinder/bot/rules/mention.py +8 -7
- telegrinder/bot/rules/message_entities.py +8 -4
- telegrinder/bot/rules/node.py +23 -12
- telegrinder/bot/rules/payload.py +5 -4
- telegrinder/bot/rules/payment_invoice.py +6 -21
- telegrinder/bot/rules/regex.py +2 -4
- telegrinder/bot/rules/rule_enum.py +8 -7
- telegrinder/bot/rules/start.py +5 -6
- telegrinder/bot/rules/state.py +1 -1
- telegrinder/bot/rules/text.py +4 -15
- telegrinder/bot/rules/update.py +3 -4
- telegrinder/bot/scenario/__init__.py +0 -0
- telegrinder/bot/scenario/abc.py +6 -5
- telegrinder/bot/scenario/checkbox.py +1 -1
- telegrinder/bot/scenario/choice.py +30 -39
- telegrinder/client/__init__.py +3 -5
- telegrinder/client/abc.py +11 -6
- telegrinder/client/aiohttp.py +141 -27
- telegrinder/client/form_data.py +1 -1
- telegrinder/model.py +61 -89
- telegrinder/modules.py +325 -102
- telegrinder/msgspec_utils/__init__.py +40 -0
- telegrinder/msgspec_utils/abc.py +18 -0
- telegrinder/msgspec_utils/custom_types/__init__.py +6 -0
- telegrinder/msgspec_utils/custom_types/datetime.py +24 -0
- telegrinder/msgspec_utils/custom_types/enum_meta.py +43 -0
- telegrinder/msgspec_utils/custom_types/literal.py +25 -0
- telegrinder/msgspec_utils/custom_types/option.py +17 -0
- telegrinder/msgspec_utils/decoder.py +389 -0
- telegrinder/msgspec_utils/encoder.py +206 -0
- telegrinder/{msgspec_json.py → msgspec_utils/json.py} +6 -5
- telegrinder/msgspec_utils/tools.py +75 -0
- telegrinder/node/__init__.py +24 -7
- telegrinder/node/attachment.py +1 -0
- telegrinder/node/base.py +154 -72
- telegrinder/node/callback_query.py +5 -5
- telegrinder/node/collection.py +39 -0
- telegrinder/node/command.py +1 -2
- telegrinder/node/composer.py +121 -72
- telegrinder/node/container.py +11 -8
- telegrinder/node/context.py +48 -0
- telegrinder/node/either.py +27 -40
- telegrinder/node/error.py +41 -0
- telegrinder/node/event.py +37 -11
- telegrinder/node/exceptions.py +7 -0
- telegrinder/node/file.py +0 -0
- telegrinder/node/i18n.py +108 -0
- telegrinder/node/me.py +3 -2
- telegrinder/node/payload.py +1 -1
- telegrinder/node/polymorphic.py +63 -28
- telegrinder/node/reply_message.py +12 -0
- telegrinder/node/rule.py +6 -13
- telegrinder/node/scope.py +14 -5
- telegrinder/node/session.py +53 -0
- telegrinder/node/source.py +41 -9
- telegrinder/node/text.py +1 -2
- telegrinder/node/tools/__init__.py +0 -0
- telegrinder/node/tools/generator.py +3 -5
- telegrinder/node/utility.py +16 -0
- telegrinder/py.typed +0 -0
- telegrinder/rules.py +0 -0
- telegrinder/tools/__init__.py +48 -88
- telegrinder/tools/aio.py +103 -0
- telegrinder/tools/callback_data_serialization/__init__.py +5 -0
- telegrinder/tools/{callback_data_serilization → callback_data_serialization}/abc.py +0 -0
- telegrinder/tools/{callback_data_serilization → callback_data_serialization}/json_ser.py +2 -3
- telegrinder/tools/{callback_data_serilization → callback_data_serialization}/msgpack_ser.py +45 -27
- telegrinder/tools/final.py +21 -0
- telegrinder/tools/formatting/__init__.py +2 -18
- telegrinder/tools/formatting/deep_links/__init__.py +39 -0
- telegrinder/tools/formatting/{deep_links.py → deep_links/links.py} +12 -85
- telegrinder/tools/formatting/deep_links/parsing.py +90 -0
- telegrinder/tools/formatting/deep_links/validators.py +8 -0
- telegrinder/tools/formatting/html_formatter.py +18 -45
- telegrinder/tools/fullname.py +83 -0
- telegrinder/tools/global_context/__init__.py +4 -3
- telegrinder/tools/global_context/abc.py +17 -14
- telegrinder/tools/global_context/builtin_context.py +39 -0
- telegrinder/tools/global_context/global_context.py +138 -39
- telegrinder/tools/input_file_directory.py +0 -0
- telegrinder/tools/keyboard/__init__.py +39 -0
- telegrinder/tools/keyboard/abc.py +159 -0
- telegrinder/tools/keyboard/base.py +77 -0
- telegrinder/tools/keyboard/buttons/__init__.py +14 -0
- telegrinder/tools/keyboard/buttons/base.py +18 -0
- telegrinder/tools/{buttons.py → keyboard/buttons/buttons.py} +71 -23
- telegrinder/tools/keyboard/buttons/static_buttons.py +56 -0
- telegrinder/tools/keyboard/buttons/tools.py +18 -0
- telegrinder/tools/keyboard/data.py +20 -0
- telegrinder/tools/keyboard/keyboard.py +131 -0
- telegrinder/tools/keyboard/static_keyboard.py +83 -0
- telegrinder/tools/lifespan.py +87 -51
- telegrinder/tools/limited_dict.py +4 -1
- telegrinder/tools/loop_wrapper.py +332 -0
- telegrinder/tools/magic/__init__.py +32 -0
- telegrinder/tools/magic/annotations.py +165 -0
- telegrinder/tools/magic/dictionary.py +20 -0
- telegrinder/tools/magic/function.py +246 -0
- telegrinder/tools/magic/shortcut.py +111 -0
- telegrinder/tools/parse_mode.py +9 -3
- telegrinder/tools/singleton/__init__.py +4 -0
- telegrinder/tools/singleton/abc.py +14 -0
- telegrinder/tools/singleton/singleton.py +18 -0
- telegrinder/tools/state_storage/__init__.py +0 -0
- telegrinder/tools/state_storage/abc.py +6 -1
- telegrinder/tools/state_storage/memory.py +1 -1
- telegrinder/tools/strings.py +0 -0
- telegrinder/types/__init__.py +307 -268
- telegrinder/types/enums.py +68 -37
- telegrinder/types/input_file.py +3 -3
- telegrinder/types/methods.py +5699 -5055
- telegrinder/types/methods_utils.py +62 -0
- telegrinder/types/objects.py +1782 -994
- telegrinder/verification_utils.py +3 -1
- telegrinder-0.5.1.dist-info/METADATA +162 -0
- telegrinder-0.5.1.dist-info/RECORD +200 -0
- {telegrinder-0.4.2.dist-info → telegrinder-0.5.1.dist-info}/licenses/LICENSE +2 -2
- telegrinder/bot/dispatch/waiter_machine/hasher/state.py +0 -20
- telegrinder/bot/rules/id.py +0 -24
- telegrinder/bot/rules/message.py +0 -15
- telegrinder/client/sonic.py +0 -212
- telegrinder/msgspec_utils.py +0 -478
- telegrinder/tools/adapter/__init__.py +0 -19
- telegrinder/tools/adapter/abc.py +0 -49
- telegrinder/tools/adapter/dataclass.py +0 -56
- telegrinder/tools/adapter/errors.py +0 -5
- telegrinder/tools/adapter/event.py +0 -61
- telegrinder/tools/adapter/node.py +0 -46
- telegrinder/tools/adapter/raw_event.py +0 -27
- telegrinder/tools/adapter/raw_update.py +0 -30
- telegrinder/tools/callback_data_serilization/__init__.py +0 -5
- telegrinder/tools/error_handler/__init__.py +0 -10
- telegrinder/tools/error_handler/abc.py +0 -30
- telegrinder/tools/error_handler/error.py +0 -9
- telegrinder/tools/error_handler/error_handler.py +0 -179
- telegrinder/tools/formatting/spec_html_formats.py +0 -75
- telegrinder/tools/functional.py +0 -8
- telegrinder/tools/global_context/telegrinder_ctx.py +0 -27
- telegrinder/tools/i18n/__init__.py +0 -12
- telegrinder/tools/i18n/abc.py +0 -32
- telegrinder/tools/i18n/middleware/__init__.py +0 -3
- telegrinder/tools/i18n/middleware/abc.py +0 -22
- telegrinder/tools/i18n/simple.py +0 -43
- telegrinder/tools/keyboard.py +0 -132
- telegrinder/tools/loop_wrapper/__init__.py +0 -4
- telegrinder/tools/loop_wrapper/abc.py +0 -20
- telegrinder/tools/loop_wrapper/loop_wrapper.py +0 -169
- telegrinder/tools/magic.py +0 -344
- telegrinder-0.4.2.dist-info/METADATA +0 -151
- telegrinder-0.4.2.dist-info/RECORD +0 -182
- {telegrinder-0.4.2.dist-info → telegrinder-0.5.1.dist-info}/WHEEL +0 -0
telegrinder/node/composer.py
CHANGED
|
@@ -1,133 +1,144 @@
|
|
|
1
|
-
import dataclasses
|
|
2
|
-
import inspect
|
|
3
1
|
import typing
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from typing import Any
|
|
4
4
|
|
|
5
|
-
from fntypes.
|
|
5
|
+
from fntypes.option import Some
|
|
6
6
|
from fntypes.result import Error, Ok, Result
|
|
7
7
|
|
|
8
8
|
from telegrinder.bot.dispatch.context import Context
|
|
9
|
-
from telegrinder.modules import logger
|
|
10
9
|
from telegrinder.node.base import (
|
|
10
|
+
AnyNode,
|
|
11
11
|
ComposeError,
|
|
12
12
|
IsNode,
|
|
13
13
|
Name,
|
|
14
|
-
|
|
14
|
+
NodeClass,
|
|
15
15
|
NodeScope,
|
|
16
|
-
|
|
16
|
+
as_node,
|
|
17
17
|
unwrap_node,
|
|
18
18
|
)
|
|
19
|
-
from telegrinder.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
from telegrinder.node.context import get_global_session, set_global_session
|
|
20
|
+
from telegrinder.node.scope import get_scope
|
|
21
|
+
from telegrinder.node.session import NodeSession
|
|
22
|
+
from telegrinder.tools.aio import Generator, maybe_awaitable, next_generator
|
|
23
|
+
from telegrinder.tools.fullname import fullname
|
|
24
|
+
from telegrinder.tools.global_context.builtin_context import TelegrinderContext
|
|
25
|
+
from telegrinder.tools.magic import bundle, get_func_annotations, join_dicts
|
|
26
|
+
|
|
27
|
+
type ComposeGenerator = Generator[typing.Any, typing.Any, typing.Any]
|
|
28
|
+
type Impls = dict[type[typing.Any], type[typing.Any]]
|
|
29
|
+
|
|
30
|
+
CONTEXT_STORE_NODES_KEY: typing.Final[str] = "_node_ctx"
|
|
31
|
+
GLOBAL_VALUE_KEY: typing.Final[str] = "_value"
|
|
32
|
+
TELEGRINDER_CONTEXT: typing.Final[TelegrinderContext] = TelegrinderContext()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_impls(
|
|
36
|
+
compose_function: typing.Callable[..., typing.Any],
|
|
37
|
+
impls: Impls,
|
|
38
|
+
/,
|
|
39
|
+
) -> dict[str, typing.Any]:
|
|
40
|
+
return {
|
|
41
|
+
key: impls[tp]
|
|
42
|
+
for key, annotation in get_func_annotations(compose_function).items()
|
|
43
|
+
if typing.get_origin(annotation) is type
|
|
44
|
+
and (typing.get_origin(tp := typing.get_args(annotation)[0]) or tp) in impls
|
|
45
|
+
}
|
|
29
46
|
|
|
30
47
|
|
|
31
48
|
async def compose_node(
|
|
32
|
-
node:
|
|
49
|
+
node: IsNode,
|
|
33
50
|
linked: dict[type[typing.Any], typing.Any],
|
|
34
51
|
data: dict[type[typing.Any], typing.Any] | None = None,
|
|
52
|
+
impls: Impls | None = None,
|
|
35
53
|
) -> "NodeSession":
|
|
36
54
|
subnodes = node.get_subnodes()
|
|
37
|
-
|
|
55
|
+
compose = bundle(node.compose, join_dicts(subnodes, linked), bundle_kwargs=True)
|
|
38
56
|
|
|
39
|
-
# Linking data via typebundle
|
|
40
57
|
if data:
|
|
41
|
-
|
|
58
|
+
compose &= bundle(node.compose, data, typebundle=True)
|
|
42
59
|
|
|
60
|
+
if impls:
|
|
61
|
+
compose &= bundle(node.compose, get_impls(node.compose, impls))
|
|
62
|
+
|
|
63
|
+
result = compose()
|
|
43
64
|
if node.is_generator():
|
|
44
|
-
generator = typing.cast(
|
|
45
|
-
value = await generator
|
|
65
|
+
generator = typing.cast("ComposeGenerator", result)
|
|
66
|
+
value = await next_generator(generator)
|
|
46
67
|
else:
|
|
47
68
|
generator = None
|
|
48
|
-
value =
|
|
49
|
-
if inspect.isawaitable(value):
|
|
50
|
-
value = await value
|
|
69
|
+
value = await maybe_awaitable(result)
|
|
51
70
|
|
|
52
|
-
return NodeSession(node, value,
|
|
71
|
+
return NodeSession(node, value, generator)
|
|
53
72
|
|
|
54
73
|
|
|
55
74
|
async def compose_nodes(
|
|
56
|
-
nodes: typing.Mapping[str,
|
|
75
|
+
nodes: typing.Mapping[str, AnyNode],
|
|
57
76
|
ctx: Context,
|
|
58
77
|
data: dict[type[typing.Any], typing.Any] | None = None,
|
|
78
|
+
impls: Impls | None = None,
|
|
59
79
|
) -> Result["NodeCollection", ComposeError]:
|
|
60
|
-
|
|
61
|
-
|
|
80
|
+
impls = impls or TELEGRINDER_CONTEXT.composer.unwrap().selected_impls
|
|
62
81
|
data = {Context: ctx} | (data or {})
|
|
63
82
|
parent_nodes = dict[IsNode, NodeSession]()
|
|
64
83
|
event_nodes: dict[IsNode, NodeSession] = ctx.get_or_set(CONTEXT_STORE_NODES_KEY, {})
|
|
65
|
-
unwrapped_nodes = {(key, n :=
|
|
84
|
+
unwrapped_nodes = {(key, n := (as_node(node)), node): unwrap_node(n) for key, node in nodes.items()}
|
|
85
|
+
|
|
86
|
+
for (parent_node_name, parent_node_t, parent_original_type), linked_nodes in unwrapped_nodes.items():
|
|
87
|
+
data.update({Name: parent_node_name, NodeClass: parent_original_type})
|
|
66
88
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
subnodes = {}
|
|
70
|
-
data[Name] = parent_node_name
|
|
89
|
+
local_nodes = LocalNodeCollection()
|
|
90
|
+
subnodes = dict[Any, Any]()
|
|
71
91
|
|
|
72
92
|
for node_t in linked_nodes:
|
|
93
|
+
node_t = as_node(node_t)
|
|
73
94
|
scope = get_scope(node_t)
|
|
74
95
|
|
|
75
96
|
if scope is NodeScope.PER_EVENT and node_t in event_nodes:
|
|
76
97
|
local_nodes[node_t] = event_nodes[node_t]
|
|
77
98
|
continue
|
|
78
|
-
|
|
79
|
-
|
|
99
|
+
|
|
100
|
+
if scope is NodeScope.GLOBAL and (global_session := get_global_session(node_t)) is not None:
|
|
101
|
+
local_nodes[node_t] = global_session
|
|
80
102
|
continue
|
|
81
103
|
|
|
82
104
|
subnodes |= {
|
|
83
105
|
k: session.value for k, session in (local_nodes | event_nodes).items() if k not in subnodes
|
|
84
106
|
}
|
|
85
107
|
try:
|
|
86
|
-
|
|
87
|
-
except
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
108
|
+
session = await compose_node(node_t, linked=subnodes, data=data, impls=impls)
|
|
109
|
+
except ComposeError as error:
|
|
110
|
+
await local_nodes.close_local_sessions()
|
|
111
|
+
return Error(ComposeError(f"Cannot compose node `{fullname(node_t)}`, error: {error.message!r}"))
|
|
112
|
+
|
|
113
|
+
if scope is NodeScope.PER_CALL:
|
|
114
|
+
await local_nodes.set_local_session(node_t, session)
|
|
115
|
+
else:
|
|
116
|
+
local_nodes[node_t] = session
|
|
92
117
|
|
|
93
118
|
if scope is NodeScope.PER_EVENT:
|
|
94
|
-
event_nodes[node_t] =
|
|
119
|
+
event_nodes[node_t] = session
|
|
95
120
|
elif scope is NodeScope.GLOBAL:
|
|
96
|
-
|
|
121
|
+
set_global_session(node_t, session)
|
|
97
122
|
|
|
98
123
|
parent_nodes[parent_node_t] = local_nodes[parent_node_t]
|
|
99
124
|
|
|
100
|
-
return Ok(NodeCollection({k: parent_nodes[t] for k, t in unwrapped_nodes}))
|
|
125
|
+
return Ok(NodeCollection(sessions={k: parent_nodes[t] for k, t, _ in unwrapped_nodes}))
|
|
101
126
|
|
|
102
127
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
value: typing.Any
|
|
107
|
-
subnodes: dict[str, typing.Self]
|
|
108
|
-
generator: typing.AsyncGenerator[typing.Any, typing.Any | None] | None = None
|
|
109
|
-
|
|
110
|
-
def __repr__(self) -> str:
|
|
111
|
-
return f"<{self.__class__.__name__}: {self.value!r}" + (" (ACTIVE)>" if self.generator else ">")
|
|
128
|
+
class LocalNodeCollection(dict[IsNode, NodeSession]):
|
|
129
|
+
def __init__(self) -> None:
|
|
130
|
+
super().__init__()
|
|
112
131
|
|
|
113
|
-
async def
|
|
114
|
-
self
|
|
115
|
-
|
|
116
|
-
scopes: tuple[NodeScope, ...] = (NodeScope.PER_CALL,),
|
|
117
|
-
) -> None:
|
|
118
|
-
if self.node_type and getattr(self.node_type, "scope", None) not in scopes:
|
|
119
|
-
return
|
|
132
|
+
async def set_local_session(self, node: IsNode, session: NodeSession, /) -> NodeSession:
|
|
133
|
+
if node in self:
|
|
134
|
+
await self[node].close()
|
|
120
135
|
|
|
121
|
-
|
|
122
|
-
|
|
136
|
+
self[node] = session
|
|
137
|
+
return session
|
|
123
138
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
logger.debug("Closing session for node {!r}...", self.node_type)
|
|
128
|
-
await self.generator.asend(with_value)
|
|
129
|
-
except StopAsyncIteration:
|
|
130
|
-
self.generator = None
|
|
139
|
+
async def close_local_sessions(self) -> None:
|
|
140
|
+
for session in self.values():
|
|
141
|
+
await session.close(scopes=(NodeScope.PER_CALL,))
|
|
131
142
|
|
|
132
143
|
|
|
133
144
|
class NodeCollection:
|
|
@@ -138,7 +149,7 @@ class NodeCollection:
|
|
|
138
149
|
self._values: dict[str, typing.Any] = {}
|
|
139
150
|
|
|
140
151
|
def __repr__(self) -> str:
|
|
141
|
-
return "<{}: sessions={!r}>".format(self.
|
|
152
|
+
return "<{}: sessions={!r}>".format(type(self).__name__, self.sessions)
|
|
142
153
|
|
|
143
154
|
@property
|
|
144
155
|
def values(self) -> dict[str, typing.Any]:
|
|
@@ -160,4 +171,42 @@ class NodeCollection:
|
|
|
160
171
|
await session.close(with_value, scopes=scopes)
|
|
161
172
|
|
|
162
173
|
|
|
163
|
-
|
|
174
|
+
class Composer:
|
|
175
|
+
impls: dict[type[typing.Any], set[typing.Any]]
|
|
176
|
+
selected_impls: Impls
|
|
177
|
+
|
|
178
|
+
def __init__(self) -> None:
|
|
179
|
+
self.impls = defaultdict(set)
|
|
180
|
+
self.selected_impls = dict()
|
|
181
|
+
|
|
182
|
+
def __setitem__(self, for_type: typing.Any, impl: type[typing.Any], /) -> None:
|
|
183
|
+
for_type = typing.get_origin(for_type) or for_type
|
|
184
|
+
|
|
185
|
+
if for_type not in self.impls:
|
|
186
|
+
raise LookupError(f"No impls defined for type of `{fullname(for_type)}`.")
|
|
187
|
+
|
|
188
|
+
if (typing.get_origin(impl) or impl) not in self.impls[for_type]:
|
|
189
|
+
raise LookupError(f"Impl `{fullname(impl)}` is not defined for type of `{fullname(for_type)}`.")
|
|
190
|
+
|
|
191
|
+
self.selected_impls[for_type] = impl
|
|
192
|
+
|
|
193
|
+
def impl[T](self, for_type: typing.Any, /) -> typing.Callable[[type[T]], type[T]]:
|
|
194
|
+
def decorator(impl: type[T], /) -> type[T]:
|
|
195
|
+
self.impls[typing.get_origin(for_type) or for_type].add(impl)
|
|
196
|
+
return impl
|
|
197
|
+
|
|
198
|
+
return decorator
|
|
199
|
+
|
|
200
|
+
async def compose_nodes(
|
|
201
|
+
self,
|
|
202
|
+
nodes: typing.Mapping[str, AnyNode],
|
|
203
|
+
ctx: Context,
|
|
204
|
+
data: dict[type[typing.Any], typing.Any] | None = None,
|
|
205
|
+
) -> Result[NodeCollection, ComposeError]:
|
|
206
|
+
return await compose_nodes(nodes, ctx, data, self.selected_impls)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
TELEGRINDER_CONTEXT.composer = Some(Composer())
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
__all__ = ("Composer", "NodeCollection", "compose_node", "compose_nodes")
|
telegrinder/node/container.py
CHANGED
|
@@ -7,6 +7,8 @@ class ContainerNode(Node):
|
|
|
7
7
|
linked_nodes: typing.ClassVar[list[IsNode]]
|
|
8
8
|
composer: typing.Callable[..., typing.Awaitable[typing.Any]]
|
|
9
9
|
|
|
10
|
+
__subnodes__: typing.ClassVar[dict[str, IsNode] | None] = None
|
|
11
|
+
|
|
10
12
|
@classmethod
|
|
11
13
|
async def compose(cls, **kw: typing.Any) -> typing.Any:
|
|
12
14
|
subnodes = cls.get_subnodes().keys()
|
|
@@ -14,20 +16,21 @@ class ContainerNode(Node):
|
|
|
14
16
|
|
|
15
17
|
@classmethod
|
|
16
18
|
def get_subnodes(cls) -> dict[str, IsNode]:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
setattr(cls, "subnodes", subnodes_dct)
|
|
21
|
-
return subnodes_dct
|
|
22
|
-
return subnodes
|
|
19
|
+
if cls.__subnodes__ is None:
|
|
20
|
+
cls.__subnodes__ = {f"node_{i}": node_t for i, node_t in enumerate(cls.linked_nodes, start=1)}
|
|
21
|
+
return cls.__subnodes__
|
|
23
22
|
|
|
24
23
|
@classmethod
|
|
25
24
|
def link_nodes(
|
|
26
25
|
cls,
|
|
27
26
|
linked_nodes: list[IsNode],
|
|
28
27
|
composer: typing.Callable[..., typing.Awaitable[typing.Any]],
|
|
29
|
-
) -> type[
|
|
30
|
-
return type(
|
|
28
|
+
) -> type[typing.Self]:
|
|
29
|
+
return type(
|
|
30
|
+
cls.__name__,
|
|
31
|
+
(cls,),
|
|
32
|
+
{"linked_nodes": linked_nodes, "composer": classmethod(composer), "__module__": __name__},
|
|
33
|
+
) # type: ignore
|
|
31
34
|
|
|
32
35
|
|
|
33
36
|
__all__ = ("ContainerNode",)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from telegrinder.node.scope import NodeScope
|
|
6
|
+
from telegrinder.tools.global_context import GlobalContext, ctx_var
|
|
7
|
+
from telegrinder.tools.global_context.builtin_context import TelegrinderContext
|
|
8
|
+
|
|
9
|
+
if typing.TYPE_CHECKING:
|
|
10
|
+
from telegrinder.node.base import IsNode
|
|
11
|
+
from telegrinder.node.composer import NodeSession
|
|
12
|
+
|
|
13
|
+
TELEGRINDER_CONTEXT: typing.Final[TelegrinderContext] = TelegrinderContext()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NodeGlobalContext(GlobalContext):
|
|
17
|
+
__ctx_name__ = "node_context"
|
|
18
|
+
|
|
19
|
+
global_sessions: dict[IsNode, NodeSession] = ctx_var(
|
|
20
|
+
const=True,
|
|
21
|
+
init=False,
|
|
22
|
+
default_factory=dict,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
async def close_global_scopes(self) -> None:
|
|
26
|
+
for session in self.global_sessions.values():
|
|
27
|
+
await session.close(scopes=(NodeScope.GLOBAL,))
|
|
28
|
+
|
|
29
|
+
self.global_sessions.clear()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_global_session(node: IsNode, /) -> NodeSession | None:
|
|
33
|
+
return NODE_CONTEXT.global_sessions.get(node)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def set_global_session(node: IsNode, session: NodeSession, /) -> None:
|
|
37
|
+
NODE_CONTEXT.global_sessions[node] = session
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@TELEGRINDER_CONTEXT.loop_wrapper.lifespan.on_shutdown
|
|
41
|
+
async def close_nodes_global_scopes() -> None:
|
|
42
|
+
await NODE_CONTEXT.close_global_scopes()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
NODE_CONTEXT: typing.Final[NodeGlobalContext] = NodeGlobalContext()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
__all__ = ("NODE_CONTEXT", "NodeGlobalContext", "get_global_session", "set_global_session")
|
telegrinder/node/either.py
CHANGED
|
@@ -4,9 +4,10 @@ from fntypes.result import Ok
|
|
|
4
4
|
|
|
5
5
|
from telegrinder.api.api import API
|
|
6
6
|
from telegrinder.bot.dispatch.context import Context
|
|
7
|
-
from telegrinder.node.base import ComposeError, FactoryNode,
|
|
8
|
-
from telegrinder.node.composer import
|
|
9
|
-
from telegrinder.node.scope import
|
|
7
|
+
from telegrinder.node.base import ComposeError, FactoryNode, IsNode
|
|
8
|
+
from telegrinder.node.composer import compose_nodes
|
|
9
|
+
from telegrinder.node.scope import per_call
|
|
10
|
+
from telegrinder.tools.fullname import fullname
|
|
10
11
|
from telegrinder.types.objects import Update
|
|
11
12
|
|
|
12
13
|
|
|
@@ -26,57 +27,43 @@ class _Either(FactoryNode):
|
|
|
26
27
|
```
|
|
27
28
|
"""
|
|
28
29
|
|
|
29
|
-
nodes: tuple[
|
|
30
|
+
nodes: tuple[IsNode, IsNode | None]
|
|
30
31
|
|
|
31
|
-
def __class_getitem__(cls, node:
|
|
32
|
+
def __class_getitem__(cls, node: IsNode | tuple[IsNode, IsNode], /) -> typing.Any:
|
|
32
33
|
nodes = (node, None) if not isinstance(node, tuple) else node
|
|
33
34
|
assert len(nodes) == 2, "Node `Either` must have at least two nodes."
|
|
34
35
|
return cls(nodes=nodes)
|
|
35
36
|
|
|
36
37
|
@classmethod
|
|
37
|
-
async def compose(cls, api: API, update: Update, context: Context) -> typing.Any
|
|
38
|
-
|
|
39
|
-
node_ctx = context.get_or_set(CONTEXT_STORE_NODES_KEY, {})
|
|
38
|
+
async def compose(cls, api: API, update: Update, context: Context) -> typing.Any:
|
|
39
|
+
composed = True
|
|
40
40
|
|
|
41
41
|
for node in cls.nodes:
|
|
42
|
-
if node is None:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
data=data,
|
|
60
|
-
)
|
|
61
|
-
except ComposeError:
|
|
62
|
-
continue
|
|
63
|
-
|
|
64
|
-
if node.scope is NodeScope.PER_EVENT:
|
|
65
|
-
node_ctx[node] = session
|
|
66
|
-
elif node.scope is NodeScope.GLOBAL:
|
|
67
|
-
setattr(node, GLOBAL_VALUE_KEY, session.value)
|
|
68
|
-
|
|
69
|
-
return session.value
|
|
70
|
-
|
|
71
|
-
raise ComposeError("Cannot compose either nodes: {}.".format(", ".join(repr(n) for n in cls.nodes)))
|
|
42
|
+
if node is not None:
|
|
43
|
+
match await compose_nodes(dict(node_result=node), context, {API: api, Update: update}):
|
|
44
|
+
case Ok(col):
|
|
45
|
+
value = col.values["node_result"]
|
|
46
|
+
yield value
|
|
47
|
+
await col.close_all(with_value=value)
|
|
48
|
+
break
|
|
49
|
+
case _:
|
|
50
|
+
composed = False
|
|
51
|
+
else:
|
|
52
|
+
yield None
|
|
53
|
+
break
|
|
54
|
+
|
|
55
|
+
if not composed:
|
|
56
|
+
raise ComposeError(
|
|
57
|
+
"Cannot compose either nodes: {}.".format(", ".join(fullname(n) for n in cls.nodes))
|
|
58
|
+
)
|
|
72
59
|
|
|
73
60
|
|
|
74
61
|
if typing.TYPE_CHECKING:
|
|
75
62
|
type Either[Left, Right: typing.Any | None] = Left | Right
|
|
76
63
|
type Optional[Left] = Either[Left, None]
|
|
77
64
|
else:
|
|
78
|
-
Either = _Either
|
|
79
|
-
Optional = type("Optional", (
|
|
65
|
+
Either = type("Either", (_Either,), {"__module__": __name__})
|
|
66
|
+
Optional = type("Optional", (_Either,), {"__module__": __name__})
|
|
80
67
|
|
|
81
68
|
|
|
82
69
|
__all__ = ("Either", "Optional")
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
|
|
3
|
+
import typing_extensions as typing
|
|
4
|
+
|
|
5
|
+
from telegrinder.bot.dispatch.context import Context
|
|
6
|
+
from telegrinder.node.base import ComposeError, DataNode
|
|
7
|
+
from telegrinder.node.utility import TypeArgs
|
|
8
|
+
|
|
9
|
+
type ExceptionType = type[BaseException]
|
|
10
|
+
|
|
11
|
+
ExceptionT = typing.TypeVar("ExceptionT", bound=BaseException, default=BaseException)
|
|
12
|
+
ExceptionTs = typing.TypeVarTuple("ExceptionTs", default=typing.Unpack[tuple[BaseException, ...]])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def can_catch[ExceptionT: BaseException](
|
|
16
|
+
exc: BaseException | ExceptionType,
|
|
17
|
+
exc_types: type[ExceptionT] | tuple[type[ExceptionT], ...],
|
|
18
|
+
) -> typing.TypeGuard[ExceptionT]:
|
|
19
|
+
return issubclass(exc, exc_types) if isinstance(exc, type) else isinstance(exc, exc_types)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
23
|
+
class Error(DataNode, typing.Generic[*ExceptionTs]):
|
|
24
|
+
exception_update: BaseException
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def exception(self: "Error[*tuple[ExceptionT, ...]]") -> ExceptionT:
|
|
28
|
+
return self.exception_update # type: ignore
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def compose(cls, ctx: Context, type_args: TypeArgs) -> typing.Self:
|
|
32
|
+
exception = ctx.exception_update.expect(ComposeError("No exception."))
|
|
33
|
+
exception_types: tuple[ExceptionType, ...] | None = type_args.get(ExceptionTs)
|
|
34
|
+
|
|
35
|
+
if exception_types is None or can_catch(exception, exception_types):
|
|
36
|
+
return cls(exception_update=exception)
|
|
37
|
+
|
|
38
|
+
raise ComposeError("Foreign exception.")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
__all__ = ("Error",)
|
telegrinder/node/event.py
CHANGED
|
@@ -2,12 +2,16 @@ import dataclasses
|
|
|
2
2
|
import typing
|
|
3
3
|
|
|
4
4
|
import msgspec
|
|
5
|
+
from fntypes.option import Some
|
|
5
6
|
|
|
6
7
|
from telegrinder.api.api import API
|
|
7
|
-
from telegrinder.bot.cute_types import BaseCute
|
|
8
|
+
from telegrinder.bot.cute_types.base import BaseCute
|
|
9
|
+
from telegrinder.bot.cute_types.update import UpdateCute
|
|
10
|
+
from telegrinder.bot.dispatch.context import Context
|
|
8
11
|
from telegrinder.msgspec_utils import decoder
|
|
9
|
-
from telegrinder.node.base import ComposeError, FactoryNode
|
|
10
|
-
from telegrinder.
|
|
12
|
+
from telegrinder.node.base import ComposeError, FactoryNode, scalar_node
|
|
13
|
+
from telegrinder.tools.fullname import fullname
|
|
14
|
+
from telegrinder.types.objects import Model, Update
|
|
11
15
|
|
|
12
16
|
if typing.TYPE_CHECKING:
|
|
13
17
|
from _typeshed import DataclassInstance
|
|
@@ -15,6 +19,17 @@ if typing.TYPE_CHECKING:
|
|
|
15
19
|
type DataclassType = DataclassInstance | msgspec.Struct | dict[str, typing.Any]
|
|
16
20
|
|
|
17
21
|
|
|
22
|
+
@scalar_node
|
|
23
|
+
class UpdateNode:
|
|
24
|
+
@classmethod
|
|
25
|
+
def compose(cls, update: Update, api: API, context: Context) -> UpdateCute:
|
|
26
|
+
match context.update_cute:
|
|
27
|
+
case Some(update_cute):
|
|
28
|
+
return update_cute
|
|
29
|
+
case _:
|
|
30
|
+
return context.add_update_cute(update, api).update_cute.unwrap()
|
|
31
|
+
|
|
32
|
+
|
|
18
33
|
class _EventNode(FactoryNode):
|
|
19
34
|
dataclass: type[DataclassType]
|
|
20
35
|
orig_dataclass: type[DataclassType]
|
|
@@ -23,13 +38,24 @@ class _EventNode(FactoryNode):
|
|
|
23
38
|
return cls(dataclass=dataclass, orig_dataclass=typing.get_origin(dataclass) or dataclass)
|
|
24
39
|
|
|
25
40
|
@classmethod
|
|
26
|
-
def compose(cls, raw_update: Update, api: API) -> DataclassType:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
update = raw_update if issubclass(cls.orig_dataclass, Update) else raw_update.incoming_update
|
|
30
|
-
dataclass = cls.orig_dataclass.from_update(update=update, bound_api=api)
|
|
41
|
+
def compose(cls, update_cute: UpdateNode, raw_update: Update, api: API) -> DataclassType:
|
|
42
|
+
if cls.orig_dataclass is UpdateCute:
|
|
43
|
+
return update_cute
|
|
31
44
|
|
|
32
|
-
|
|
45
|
+
if issubclass(cls.orig_dataclass, BaseCute | Model):
|
|
46
|
+
incoming_update = (
|
|
47
|
+
update_cute.incoming_update
|
|
48
|
+
if issubclass(cls.orig_dataclass, BaseCute)
|
|
49
|
+
else raw_update.incoming_update
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if type(incoming_update) is not cls.orig_dataclass:
|
|
53
|
+
raise ComposeError(f"Incoming update is not `{fullname(cls.orig_dataclass)}`.")
|
|
54
|
+
|
|
55
|
+
return incoming_update
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
if issubclass(cls.orig_dataclass, msgspec.Struct) or dataclasses.is_dataclass(
|
|
33
59
|
cls.orig_dataclass,
|
|
34
60
|
):
|
|
35
61
|
dataclass = decoder.convert(
|
|
@@ -42,13 +68,13 @@ class _EventNode(FactoryNode):
|
|
|
42
68
|
|
|
43
69
|
return dataclass
|
|
44
70
|
except Exception as exc:
|
|
45
|
-
raise ComposeError(f"Cannot parse an update object into {cls.dataclass
|
|
71
|
+
raise ComposeError(f"Cannot parse an update object into `{fullname(cls.dataclass)}`, error: {exc}")
|
|
46
72
|
|
|
47
73
|
|
|
48
74
|
if typing.TYPE_CHECKING:
|
|
49
75
|
type EventNode[Dataclass: DataclassType] = Dataclass
|
|
50
76
|
else:
|
|
51
|
-
EventNode = _EventNode
|
|
77
|
+
EventNode = type("EventNode", (_EventNode,), {"__module__": __name__})
|
|
52
78
|
|
|
53
79
|
|
|
54
80
|
__all__ = ("EventNode",)
|
telegrinder/node/file.py
CHANGED
|
File without changes
|