telegrinder 0.3.4.post1__py3-none-any.whl → 0.4.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 +30 -31
- telegrinder/api/__init__.py +2 -1
- telegrinder/api/api.py +28 -20
- telegrinder/api/error.py +8 -4
- telegrinder/api/response.py +2 -2
- telegrinder/api/token.py +2 -2
- telegrinder/bot/__init__.py +6 -0
- telegrinder/bot/bot.py +38 -31
- telegrinder/bot/cute_types/__init__.py +2 -0
- telegrinder/bot/cute_types/base.py +54 -128
- telegrinder/bot/cute_types/callback_query.py +76 -61
- telegrinder/bot/cute_types/chat_join_request.py +4 -3
- telegrinder/bot/cute_types/chat_member_updated.py +28 -31
- telegrinder/bot/cute_types/inline_query.py +5 -4
- telegrinder/bot/cute_types/message.py +555 -602
- telegrinder/bot/cute_types/pre_checkout_query.py +42 -0
- telegrinder/bot/cute_types/update.py +20 -12
- telegrinder/bot/cute_types/utils.py +3 -36
- telegrinder/bot/dispatch/__init__.py +4 -0
- telegrinder/bot/dispatch/abc.py +8 -9
- telegrinder/bot/dispatch/context.py +5 -7
- telegrinder/bot/dispatch/dispatch.py +85 -33
- telegrinder/bot/dispatch/handler/abc.py +5 -6
- telegrinder/bot/dispatch/handler/audio_reply.py +2 -2
- telegrinder/bot/dispatch/handler/base.py +3 -3
- telegrinder/bot/dispatch/handler/document_reply.py +2 -2
- telegrinder/bot/dispatch/handler/func.py +36 -42
- telegrinder/bot/dispatch/handler/media_group_reply.py +5 -4
- telegrinder/bot/dispatch/handler/message_reply.py +2 -2
- telegrinder/bot/dispatch/handler/photo_reply.py +2 -2
- telegrinder/bot/dispatch/handler/sticker_reply.py +2 -2
- telegrinder/bot/dispatch/handler/video_reply.py +2 -2
- telegrinder/bot/dispatch/middleware/abc.py +83 -8
- telegrinder/bot/dispatch/middleware/global_middleware.py +70 -0
- telegrinder/bot/dispatch/process.py +44 -50
- telegrinder/bot/dispatch/return_manager/__init__.py +2 -0
- telegrinder/bot/dispatch/return_manager/abc.py +6 -10
- telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +20 -0
- telegrinder/bot/dispatch/view/__init__.py +2 -0
- telegrinder/bot/dispatch/view/abc.py +10 -6
- telegrinder/bot/dispatch/view/base.py +81 -50
- telegrinder/bot/dispatch/view/box.py +20 -9
- telegrinder/bot/dispatch/view/callback_query.py +3 -4
- telegrinder/bot/dispatch/view/chat_join_request.py +2 -7
- telegrinder/bot/dispatch/view/chat_member.py +3 -5
- telegrinder/bot/dispatch/view/inline_query.py +3 -4
- telegrinder/bot/dispatch/view/message.py +3 -4
- telegrinder/bot/dispatch/view/pre_checkout_query.py +16 -0
- telegrinder/bot/dispatch/view/raw.py +42 -40
- telegrinder/bot/dispatch/waiter_machine/actions.py +5 -4
- telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +0 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +0 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +9 -7
- telegrinder/bot/dispatch/waiter_machine/hasher/message.py +0 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/state.py +3 -2
- telegrinder/bot/dispatch/waiter_machine/machine.py +113 -34
- telegrinder/bot/dispatch/waiter_machine/middleware.py +15 -10
- telegrinder/bot/dispatch/waiter_machine/short_state.py +7 -18
- telegrinder/bot/polling/polling.py +62 -54
- telegrinder/bot/rules/__init__.py +24 -1
- telegrinder/bot/rules/abc.py +17 -10
- telegrinder/bot/rules/callback_data.py +20 -61
- telegrinder/bot/rules/chat_join.py +6 -4
- telegrinder/bot/rules/command.py +4 -4
- telegrinder/bot/rules/enum_text.py +1 -4
- telegrinder/bot/rules/func.py +5 -3
- telegrinder/bot/rules/fuzzy.py +1 -1
- telegrinder/bot/rules/id.py +24 -0
- telegrinder/bot/rules/inline.py +6 -4
- telegrinder/bot/rules/integer.py +2 -1
- telegrinder/bot/rules/logic.py +18 -0
- telegrinder/bot/rules/markup.py +5 -6
- telegrinder/bot/rules/message.py +2 -4
- telegrinder/bot/rules/message_entities.py +1 -3
- telegrinder/bot/rules/node.py +15 -9
- telegrinder/bot/rules/payload.py +81 -0
- telegrinder/bot/rules/payment_invoice.py +29 -0
- telegrinder/bot/rules/regex.py +5 -6
- telegrinder/bot/rules/state.py +1 -3
- telegrinder/bot/rules/text.py +10 -5
- telegrinder/bot/rules/update.py +0 -0
- telegrinder/bot/scenario/abc.py +2 -4
- telegrinder/bot/scenario/checkbox.py +12 -14
- telegrinder/bot/scenario/choice.py +6 -9
- telegrinder/client/__init__.py +9 -1
- telegrinder/client/abc.py +35 -10
- telegrinder/client/aiohttp.py +28 -24
- telegrinder/client/form_data.py +31 -0
- telegrinder/client/sonic.py +212 -0
- telegrinder/model.py +38 -145
- telegrinder/modules.py +3 -1
- telegrinder/msgspec_utils.py +136 -68
- telegrinder/node/__init__.py +74 -13
- telegrinder/node/attachment.py +92 -16
- telegrinder/node/base.py +196 -68
- telegrinder/node/callback_query.py +17 -16
- telegrinder/node/command.py +3 -2
- telegrinder/node/composer.py +40 -75
- telegrinder/node/container.py +13 -7
- telegrinder/node/either.py +82 -0
- telegrinder/node/event.py +20 -31
- telegrinder/node/file.py +51 -0
- telegrinder/node/me.py +4 -5
- telegrinder/node/payload.py +78 -0
- telegrinder/node/polymorphic.py +27 -8
- telegrinder/node/rule.py +2 -6
- telegrinder/node/scope.py +4 -6
- telegrinder/node/source.py +37 -21
- telegrinder/node/text.py +20 -8
- telegrinder/node/tools/generator.py +7 -11
- telegrinder/py.typed +0 -0
- telegrinder/rules.py +0 -61
- telegrinder/tools/__init__.py +97 -38
- telegrinder/tools/adapter/__init__.py +19 -0
- telegrinder/tools/adapter/abc.py +49 -0
- telegrinder/tools/adapter/dataclass.py +56 -0
- telegrinder/{bot/rules → tools}/adapter/event.py +8 -10
- telegrinder/{bot/rules → tools}/adapter/node.py +8 -10
- telegrinder/{bot/rules → tools}/adapter/raw_event.py +2 -2
- telegrinder/{bot/rules → tools}/adapter/raw_update.py +2 -2
- telegrinder/tools/buttons.py +52 -26
- telegrinder/tools/callback_data_serilization/__init__.py +5 -0
- telegrinder/tools/callback_data_serilization/abc.py +51 -0
- telegrinder/tools/callback_data_serilization/json_ser.py +60 -0
- telegrinder/tools/callback_data_serilization/msgpack_ser.py +172 -0
- telegrinder/tools/error_handler/abc.py +4 -7
- telegrinder/tools/error_handler/error.py +0 -0
- telegrinder/tools/error_handler/error_handler.py +34 -48
- telegrinder/tools/formatting/__init__.py +57 -37
- telegrinder/tools/formatting/deep_links.py +541 -0
- telegrinder/tools/formatting/{html.py → html_formatter.py} +51 -79
- telegrinder/tools/formatting/spec_html_formats.py +14 -60
- telegrinder/tools/functional.py +1 -5
- telegrinder/tools/global_context/global_context.py +26 -51
- telegrinder/tools/global_context/telegrinder_ctx.py +3 -3
- telegrinder/tools/i18n/abc.py +0 -0
- telegrinder/tools/i18n/middleware/abc.py +3 -6
- telegrinder/tools/input_file_directory.py +30 -0
- telegrinder/tools/keyboard.py +9 -9
- telegrinder/tools/lifespan.py +105 -0
- telegrinder/tools/limited_dict.py +5 -10
- telegrinder/tools/loop_wrapper/abc.py +7 -2
- telegrinder/tools/loop_wrapper/loop_wrapper.py +40 -95
- telegrinder/tools/magic.py +184 -34
- telegrinder/tools/state_storage/__init__.py +0 -0
- telegrinder/tools/state_storage/abc.py +5 -9
- telegrinder/tools/state_storage/memory.py +1 -1
- telegrinder/tools/strings.py +13 -0
- telegrinder/types/__init__.py +8 -0
- telegrinder/types/enums.py +31 -21
- telegrinder/types/input_file.py +51 -0
- telegrinder/types/methods.py +531 -109
- telegrinder/types/objects.py +934 -826
- telegrinder/verification_utils.py +0 -2
- {telegrinder-0.3.4.post1.dist-info → telegrinder-0.4.0.dist-info}/LICENSE +2 -2
- telegrinder-0.4.0.dist-info/METADATA +144 -0
- telegrinder-0.4.0.dist-info/RECORD +182 -0
- {telegrinder-0.3.4.post1.dist-info → telegrinder-0.4.0.dist-info}/WHEEL +1 -1
- telegrinder/bot/rules/adapter/__init__.py +0 -17
- telegrinder/bot/rules/adapter/abc.py +0 -31
- telegrinder/node/message.py +0 -14
- telegrinder/node/update.py +0 -15
- telegrinder/tools/formatting/links.py +0 -38
- telegrinder/tools/kb_set/__init__.py +0 -4
- telegrinder/tools/kb_set/base.py +0 -15
- telegrinder/tools/kb_set/yaml.py +0 -63
- telegrinder-0.3.4.post1.dist-info/METADATA +0 -110
- telegrinder-0.3.4.post1.dist-info/RECORD +0 -165
- /telegrinder/{bot/rules → tools}/adapter/errors.py +0 -0
telegrinder/bot/rules/markup.py
CHANGED
|
@@ -3,12 +3,13 @@ import typing
|
|
|
3
3
|
import vbml
|
|
4
4
|
|
|
5
5
|
from telegrinder.bot.dispatch.context import Context
|
|
6
|
-
from telegrinder.node.
|
|
6
|
+
from telegrinder.node.either import Either
|
|
7
|
+
from telegrinder.node.text import Caption, Text
|
|
7
8
|
from telegrinder.tools.global_context.telegrinder_ctx import TelegrinderContext
|
|
8
9
|
|
|
9
10
|
from .abc import ABCRule
|
|
10
11
|
|
|
11
|
-
PatternLike
|
|
12
|
+
type PatternLike = str | vbml.Pattern
|
|
12
13
|
global_ctx: typing.Final[TelegrinderContext] = TelegrinderContext()
|
|
13
14
|
|
|
14
15
|
|
|
@@ -30,13 +31,11 @@ class Markup(ABCRule):
|
|
|
30
31
|
if not isinstance(patterns, list):
|
|
31
32
|
patterns = [patterns]
|
|
32
33
|
self.patterns = [
|
|
33
|
-
vbml.Pattern(pattern, flags=global_ctx.vbml_pattern_flags)
|
|
34
|
-
if isinstance(pattern, str)
|
|
35
|
-
else pattern
|
|
34
|
+
vbml.Pattern(pattern, flags=global_ctx.vbml_pattern_flags) if isinstance(pattern, str) else pattern
|
|
36
35
|
for pattern in patterns
|
|
37
36
|
]
|
|
38
37
|
|
|
39
|
-
def check(self, text: Text, ctx: Context) -> bool:
|
|
38
|
+
def check(self, text: Either[Text, Caption], ctx: Context) -> bool:
|
|
40
39
|
return check_string(self.patterns, text, ctx)
|
|
41
40
|
|
|
42
41
|
|
telegrinder/bot/rules/message.py
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
import typing
|
|
3
3
|
|
|
4
|
+
from telegrinder.tools.adapter.event import EventAdapter
|
|
4
5
|
from telegrinder.types.objects import Message as MessageEvent
|
|
5
6
|
|
|
6
7
|
from .abc import ABCRule, CheckResult, Message
|
|
7
|
-
from .adapter import EventAdapter
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class MessageRule(ABCRule[Message], abc.ABC):
|
|
11
|
-
adapter: EventAdapter[Message] = EventAdapter(MessageEvent, Message)
|
|
12
|
-
|
|
10
|
+
class MessageRule(ABCRule[Message], abc.ABC, adapter=EventAdapter(MessageEvent, Message)):
|
|
13
11
|
@abc.abstractmethod
|
|
14
12
|
def check(self, *args: typing.Any, **kwargs: typing.Any) -> CheckResult: ...
|
|
15
13
|
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import typing
|
|
2
|
-
|
|
3
1
|
from telegrinder.bot.dispatch.context import Context
|
|
4
2
|
from telegrinder.types.enums import MessageEntityType
|
|
5
3
|
from telegrinder.types.objects import MessageEntity
|
|
6
4
|
|
|
7
5
|
from .message import Message, MessageRule
|
|
8
6
|
|
|
9
|
-
Entity
|
|
7
|
+
type Entity = str | MessageEntityType
|
|
10
8
|
|
|
11
9
|
|
|
12
10
|
class HasEntities(MessageRule):
|
telegrinder/bot/rules/node.py
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
|
|
3
3
|
from telegrinder.bot.dispatch.context import Context
|
|
4
|
-
from telegrinder.node.base import
|
|
4
|
+
from telegrinder.node.base import IsNode, NodeType, is_node
|
|
5
|
+
from telegrinder.tools.adapter.node import NodeAdapter
|
|
5
6
|
|
|
6
7
|
from .abc import ABCRule
|
|
7
|
-
from .adapter.node import NodeAdapter
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class NodeRule(ABCRule
|
|
11
|
-
def __init__(self, *nodes:
|
|
12
|
-
|
|
13
|
-
self.
|
|
14
|
-
|
|
10
|
+
class NodeRule(ABCRule):
|
|
11
|
+
def __init__(self, *nodes: IsNode | tuple[str, IsNode]) -> None:
|
|
12
|
+
self.nodes: list[IsNode] = []
|
|
13
|
+
self.node_keys: list[str | None] = []
|
|
14
|
+
|
|
15
|
+
for binding in nodes:
|
|
16
|
+
node_key, node_t = binding if isinstance(binding, tuple) else (None, binding)
|
|
17
|
+
if not is_node(node_t):
|
|
18
|
+
continue
|
|
19
|
+
self.nodes.append(node_t)
|
|
20
|
+
self.node_keys.append(node_key)
|
|
15
21
|
|
|
16
22
|
@property
|
|
17
23
|
def adapter(self) -> NodeAdapter:
|
|
18
|
-
return NodeAdapter(*self.nodes)
|
|
24
|
+
return NodeAdapter(*self.nodes)
|
|
19
25
|
|
|
20
|
-
def check(self, resolved_nodes: tuple[
|
|
26
|
+
def check(self, resolved_nodes: tuple[NodeType, ...], ctx: Context) -> typing.Literal[True]:
|
|
21
27
|
for i, node in enumerate(resolved_nodes):
|
|
22
28
|
if key := self.node_keys[i]:
|
|
23
29
|
ctx[key] = node
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from contextlib import suppress
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
|
|
5
|
+
import msgspec
|
|
6
|
+
|
|
7
|
+
from telegrinder.bot.dispatch.context import Context
|
|
8
|
+
from telegrinder.bot.rules.abc import ABCRule
|
|
9
|
+
from telegrinder.bot.rules.markup import Markup, PatternLike, check_string
|
|
10
|
+
from telegrinder.msgspec_json import loads
|
|
11
|
+
from telegrinder.node.base import Node
|
|
12
|
+
from telegrinder.node.payload import Payload, PayloadData
|
|
13
|
+
from telegrinder.tools.callback_data_serilization.abc import ABCDataSerializer, ModelType
|
|
14
|
+
from telegrinder.tools.callback_data_serilization.json_ser import JSONSerializer
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PayloadRule[Data](ABCRule):
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
data_type: type[Data],
|
|
21
|
+
serializer: type[ABCDataSerializer[Data]],
|
|
22
|
+
*,
|
|
23
|
+
alias: str | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
self.data_type = data_type
|
|
26
|
+
self.serializer = serializer
|
|
27
|
+
self.alias = alias or "data"
|
|
28
|
+
|
|
29
|
+
@cached_property
|
|
30
|
+
def required_nodes(self) -> dict[str, type[Node]]:
|
|
31
|
+
return {"payload": PayloadData[self.data_type, self.serializer]} # type: ignore
|
|
32
|
+
|
|
33
|
+
def check(self, payload: PayloadData[Data], context: Context) -> typing.Literal[True]:
|
|
34
|
+
context.set(self.alias, payload)
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PayloadModelRule[Model: ModelType](PayloadRule[Model]):
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
model_t: type[Model],
|
|
42
|
+
*,
|
|
43
|
+
serializer: type[ABCDataSerializer[Model]] | None = None,
|
|
44
|
+
alias: str | None = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
super().__init__(model_t, serializer or JSONSerializer, alias=alias or "model")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PayloadEqRule(ABCRule):
|
|
50
|
+
def __init__(self, payloads: str | list[str], /) -> None:
|
|
51
|
+
self.payloads = [payloads] if isinstance(payloads, str) else payloads
|
|
52
|
+
|
|
53
|
+
def check(self, payload: Payload) -> bool:
|
|
54
|
+
return any(p == payload for p in self.payloads)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class PayloadMarkupRule(ABCRule):
|
|
58
|
+
def __init__(self, pattern: PatternLike | list[PatternLike], /) -> None:
|
|
59
|
+
self.patterns = Markup(pattern).patterns
|
|
60
|
+
|
|
61
|
+
def check(self, payload: Payload, context: Context) -> bool:
|
|
62
|
+
return check_string(self.patterns, payload, context)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class PayloadJsonEqRule(ABCRule):
|
|
66
|
+
def __init__(self, payload: dict[str, typing.Any], /) -> None:
|
|
67
|
+
self.payload = payload
|
|
68
|
+
|
|
69
|
+
def check(self, payload: Payload) -> bool:
|
|
70
|
+
with suppress(msgspec.DecodeError, msgspec.ValidationError):
|
|
71
|
+
return self.payload == loads(payload)
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
__all__ = (
|
|
76
|
+
"PayloadEqRule",
|
|
77
|
+
"PayloadJsonEqRule",
|
|
78
|
+
"PayloadMarkupRule",
|
|
79
|
+
"PayloadModelRule",
|
|
80
|
+
"PayloadRule",
|
|
81
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from telegrinder.bot.cute_types.pre_checkout_query import PreCheckoutQueryCute
|
|
5
|
+
from telegrinder.bot.rules.abc import ABCRule, CheckResult
|
|
6
|
+
from telegrinder.tools.adapter.event import EventAdapter
|
|
7
|
+
from telegrinder.types.enums import Currency, UpdateType
|
|
8
|
+
|
|
9
|
+
PreCheckoutQuery: typing.TypeAlias = PreCheckoutQueryCute
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PaymentInvoiceRule(
|
|
13
|
+
ABCRule[PreCheckoutQuery],
|
|
14
|
+
abc.ABC,
|
|
15
|
+
adapter=EventAdapter(UpdateType.PRE_CHECKOUT_QUERY, PreCheckoutQuery),
|
|
16
|
+
):
|
|
17
|
+
@abc.abstractmethod
|
|
18
|
+
def check(self, *args: typing.Any, **kwargs: typing.Any) -> CheckResult: ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PaymentInvoiceCurrency(PaymentInvoiceRule):
|
|
22
|
+
def __init__(self, currency: str | Currency, /) -> None:
|
|
23
|
+
self.currency = currency
|
|
24
|
+
|
|
25
|
+
def check(self, query: PreCheckoutQuery) -> bool:
|
|
26
|
+
return self.currency == query.currency
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
__all__ = ("PaymentInvoiceCurrency", "PaymentInvoiceRule")
|
telegrinder/bot/rules/regex.py
CHANGED
|
@@ -2,11 +2,12 @@ import re
|
|
|
2
2
|
import typing
|
|
3
3
|
|
|
4
4
|
from telegrinder.bot.dispatch.context import Context
|
|
5
|
-
from telegrinder.node.
|
|
5
|
+
from telegrinder.node.either import Either
|
|
6
|
+
from telegrinder.node.text import Caption, Text
|
|
6
7
|
|
|
7
8
|
from .abc import ABCRule
|
|
8
9
|
|
|
9
|
-
PatternLike
|
|
10
|
+
type PatternLike = str | typing.Pattern[str]
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class Regex(ABCRule):
|
|
@@ -18,11 +19,9 @@ class Regex(ABCRule):
|
|
|
18
19
|
case str(regex):
|
|
19
20
|
self.regexp.append(re.compile(regex))
|
|
20
21
|
case _:
|
|
21
|
-
self.regexp.extend(
|
|
22
|
-
re.compile(regexp) if isinstance(regexp, str) else regexp for regexp in regexp
|
|
23
|
-
)
|
|
22
|
+
self.regexp.extend(re.compile(regexp) if isinstance(regexp, str) else regexp for regexp in regexp)
|
|
24
23
|
|
|
25
|
-
def check(self, text: Text, ctx: Context) -> bool:
|
|
24
|
+
def check(self, text: Either[Text, Caption], ctx: Context) -> bool:
|
|
26
25
|
for regexp in self.regexp:
|
|
27
26
|
response = re.match(regexp, text)
|
|
28
27
|
if response is not None:
|
telegrinder/bot/rules/state.py
CHANGED
|
@@ -9,8 +9,6 @@ from telegrinder.node.source import Source
|
|
|
9
9
|
if typing.TYPE_CHECKING:
|
|
10
10
|
from telegrinder.tools.state_storage.abc import ABCStateStorage
|
|
11
11
|
|
|
12
|
-
Payload = typing.TypeVar("Payload")
|
|
13
|
-
|
|
14
12
|
|
|
15
13
|
class StateMeta(enum.Enum):
|
|
16
14
|
NO_STATE = enum.auto()
|
|
@@ -18,7 +16,7 @@ class StateMeta(enum.Enum):
|
|
|
18
16
|
|
|
19
17
|
|
|
20
18
|
@dataclasses.dataclass(frozen=True, slots=True, repr=False)
|
|
21
|
-
class State
|
|
19
|
+
class State[Payload](ABCRule):
|
|
22
20
|
storage: "ABCStateStorage[Payload]"
|
|
23
21
|
key: str | StateMeta | enum.Enum
|
|
24
22
|
|
telegrinder/bot/rules/text.py
CHANGED
|
@@ -9,25 +9,30 @@ from .node import NodeRule
|
|
|
9
9
|
|
|
10
10
|
class HasText(NodeRule):
|
|
11
11
|
def __init__(self) -> None:
|
|
12
|
-
super().__init__(node.text.Text)
|
|
12
|
+
super().__init__(node.as_node(node.text.Text))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HasCaption(NodeRule):
|
|
16
|
+
def __init__(self) -> None:
|
|
17
|
+
super().__init__(node.as_node(node.text.Caption))
|
|
13
18
|
|
|
14
19
|
|
|
15
20
|
class Text(ABCRule):
|
|
16
|
-
def __init__(self, texts: str | list[str], *, ignore_case: bool = False) -> None:
|
|
21
|
+
def __init__(self, texts: str | list[str], /, *, ignore_case: bool = False) -> None:
|
|
17
22
|
if not isinstance(texts, list):
|
|
18
23
|
texts = [texts]
|
|
19
24
|
self.texts = texts if not ignore_case else list(map(str.lower, texts))
|
|
20
25
|
self.ignore_case = ignore_case
|
|
21
26
|
|
|
22
|
-
def check(self, text: node.text.Text) -> bool:
|
|
27
|
+
def check(self, text: node.either.Either[node.text.Text, node.text.Caption]) -> bool:
|
|
23
28
|
return (text if not self.ignore_case else text.lower()) in self.texts
|
|
24
29
|
|
|
25
30
|
@with_caching_translations
|
|
26
31
|
async def translate(self, translator: ABCTranslator) -> typing.Self:
|
|
27
32
|
return self.__class__(
|
|
28
|
-
|
|
33
|
+
[translator.get(text) for text in self.texts],
|
|
29
34
|
ignore_case=self.ignore_case,
|
|
30
35
|
)
|
|
31
36
|
|
|
32
37
|
|
|
33
|
-
__all__ = ("HasText", "Text")
|
|
38
|
+
__all__ = ("HasCaption", "HasText", "Text")
|
telegrinder/bot/rules/update.py
CHANGED
|
File without changes
|
telegrinder/bot/scenario/abc.py
CHANGED
|
@@ -7,12 +7,10 @@ if typing.TYPE_CHECKING:
|
|
|
7
7
|
from telegrinder.api import API
|
|
8
8
|
from telegrinder.bot.dispatch.view.abc import ABCStateView
|
|
9
9
|
|
|
10
|
-
EventT = typing.TypeVar("EventT", bound=BaseCute)
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
class ABCScenario(ABC, typing.Generic[EventT]):
|
|
11
|
+
class ABCScenario[Event: BaseCute](ABC):
|
|
14
12
|
@abstractmethod
|
|
15
|
-
def wait(self, api: "API", view: "ABCStateView[
|
|
13
|
+
def wait(self, api: "API", view: "ABCStateView[Event]") -> typing.Any:
|
|
16
14
|
pass
|
|
17
15
|
|
|
18
16
|
|
|
@@ -13,18 +13,15 @@ from telegrinder.types.objects import InlineKeyboardMarkup
|
|
|
13
13
|
|
|
14
14
|
if typing.TYPE_CHECKING:
|
|
15
15
|
from telegrinder.api.api import API
|
|
16
|
-
from telegrinder.bot.dispatch.view.base import BaseStateView
|
|
17
16
|
|
|
18
|
-
Key = typing.TypeVar("Key", bound=typing.Hashable)
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
class ChoiceCode(enum.StrEnum):
|
|
18
|
+
class ChoiceAction(enum.StrEnum):
|
|
22
19
|
READY = "ready"
|
|
23
20
|
CANCEL = "cancel"
|
|
24
21
|
|
|
25
22
|
|
|
26
23
|
@dataclasses.dataclass(slots=True)
|
|
27
|
-
class Choice
|
|
24
|
+
class Choice[Key: typing.Hashable]:
|
|
28
25
|
key: Key
|
|
29
26
|
is_picked: bool
|
|
30
27
|
default_text: str
|
|
@@ -85,14 +82,14 @@ class _Checkbox(ABCScenario[CallbackQueryCute]):
|
|
|
85
82
|
)
|
|
86
83
|
kb.row()
|
|
87
84
|
|
|
88
|
-
kb.add(InlineButton(self.ready, callback_data=self.random_code + "/" +
|
|
85
|
+
kb.add(InlineButton(self.ready, callback_data=self.random_code + "/" + ChoiceAction.READY))
|
|
89
86
|
if self.cancel_text is not None:
|
|
90
87
|
kb.row()
|
|
91
|
-
kb.add(InlineButton(self.cancel_text, callback_data=self.random_code + "/" +
|
|
88
|
+
kb.add(InlineButton(self.cancel_text, callback_data=self.random_code + "/" + ChoiceAction.CANCEL))
|
|
92
89
|
|
|
93
90
|
return kb.get_markup()
|
|
94
91
|
|
|
95
|
-
def add_option(
|
|
92
|
+
def add_option[Key: typing.Hashable](
|
|
96
93
|
self,
|
|
97
94
|
key: Key,
|
|
98
95
|
default_text: str,
|
|
@@ -109,9 +106,9 @@ class _Checkbox(ABCScenario[CallbackQueryCute]):
|
|
|
109
106
|
code = cb.data.unwrap().replace(self.random_code + "/", "", 1)
|
|
110
107
|
|
|
111
108
|
match code:
|
|
112
|
-
case
|
|
109
|
+
case ChoiceAction.READY:
|
|
113
110
|
return False
|
|
114
|
-
case
|
|
111
|
+
case ChoiceAction.CANCEL:
|
|
115
112
|
self.choices = []
|
|
116
113
|
return False
|
|
117
114
|
|
|
@@ -132,7 +129,6 @@ class _Checkbox(ABCScenario[CallbackQueryCute]):
|
|
|
132
129
|
self,
|
|
133
130
|
hasher: Hasher[CallbackQueryCute, int],
|
|
134
131
|
api: "API",
|
|
135
|
-
view: "BaseStateView[CallbackQueryCute]",
|
|
136
132
|
) -> tuple[dict[typing.Hashable, bool], int]:
|
|
137
133
|
assert len(self.choices) > 0
|
|
138
134
|
message = (
|
|
@@ -145,7 +141,10 @@ class _Checkbox(ABCScenario[CallbackQueryCute]):
|
|
|
145
141
|
).unwrap()
|
|
146
142
|
|
|
147
143
|
while True:
|
|
148
|
-
q, _ = await self.waiter_machine.wait(
|
|
144
|
+
q, _ = await self.waiter_machine.wait(
|
|
145
|
+
hasher,
|
|
146
|
+
data=message.message_id,
|
|
147
|
+
)
|
|
149
148
|
should_continue = await self.handle(q)
|
|
150
149
|
await q.answer(self.CALLBACK_ANSWER)
|
|
151
150
|
if not should_continue:
|
|
@@ -159,14 +158,13 @@ class _Checkbox(ABCScenario[CallbackQueryCute]):
|
|
|
159
158
|
|
|
160
159
|
if typing.TYPE_CHECKING:
|
|
161
160
|
|
|
162
|
-
class Checkbox
|
|
161
|
+
class Checkbox[Key: typing.Hashable](_Checkbox):
|
|
163
162
|
choices: list[Choice[Key]]
|
|
164
163
|
|
|
165
164
|
async def wait(
|
|
166
165
|
self,
|
|
167
166
|
hasher: Hasher[CallbackQueryCute, int],
|
|
168
167
|
api: "API",
|
|
169
|
-
view: "BaseStateView[CallbackQueryCute]",
|
|
170
168
|
) -> tuple[dict[Key, bool], int]: ...
|
|
171
169
|
|
|
172
170
|
else:
|
|
@@ -2,27 +2,24 @@ import typing
|
|
|
2
2
|
|
|
3
3
|
from telegrinder.bot.cute_types.callback_query import CallbackQueryCute
|
|
4
4
|
from telegrinder.bot.dispatch.waiter_machine.hasher.hasher import Hasher
|
|
5
|
-
from telegrinder.bot.scenario.checkbox import Checkbox,
|
|
5
|
+
from telegrinder.bot.scenario.checkbox import Checkbox, ChoiceAction
|
|
6
6
|
|
|
7
7
|
if typing.TYPE_CHECKING:
|
|
8
8
|
from telegrinder.api.api import API
|
|
9
9
|
from telegrinder.bot.dispatch.view.base import BaseStateView
|
|
10
|
-
from telegrinder.bot.scenario.checkbox import Key
|
|
11
10
|
|
|
12
|
-
class Choice
|
|
11
|
+
class Choice[Key: typing.Hashable](Checkbox[Key]):
|
|
13
12
|
async def wait(
|
|
14
13
|
self,
|
|
15
14
|
hasher: Hasher[CallbackQueryCute, int],
|
|
16
15
|
api: API,
|
|
17
|
-
view: BaseStateView[CallbackQueryCute],
|
|
18
16
|
) -> tuple[Key, int]: ...
|
|
19
17
|
|
|
20
18
|
else:
|
|
21
|
-
|
|
22
19
|
class Choice(Checkbox):
|
|
23
20
|
async def handle(self, cb):
|
|
24
21
|
code = cb.data.unwrap().replace(self.random_code + "/", "", 1)
|
|
25
|
-
if code ==
|
|
22
|
+
if code == ChoiceAction.READY:
|
|
26
23
|
return False
|
|
27
24
|
|
|
28
25
|
for choice in self.choices:
|
|
@@ -41,10 +38,10 @@ else:
|
|
|
41
38
|
|
|
42
39
|
return True
|
|
43
40
|
|
|
44
|
-
async def wait(self, hasher, api
|
|
41
|
+
async def wait(self, hasher, api):
|
|
45
42
|
if len(tuple(choice for choice in self.choices if choice.is_picked)) != 1:
|
|
46
|
-
raise ValueError("Exactly one choice must be picked")
|
|
47
|
-
choices, m_id = await super().wait(hasher, api
|
|
43
|
+
raise ValueError("Exactly one choice must be picked.")
|
|
44
|
+
choices, m_id = await super().wait(hasher, api)
|
|
48
45
|
return tuple(choices.keys())[tuple(choices.values()).index(True)], m_id
|
|
49
46
|
|
|
50
47
|
|
telegrinder/client/__init__.py
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
from .abc import ABCClient
|
|
2
2
|
from .aiohttp import AiohttpClient
|
|
3
|
+
from .form_data import MultipartFormProto, encode_form_data
|
|
4
|
+
from .sonic import AiosonicClient
|
|
3
5
|
|
|
4
|
-
__all__ = (
|
|
6
|
+
__all__ = (
|
|
7
|
+
"ABCClient",
|
|
8
|
+
"AiohttpClient",
|
|
9
|
+
"AiosonicClient",
|
|
10
|
+
"MultipartFormProto",
|
|
11
|
+
"encode_form_data",
|
|
12
|
+
)
|
telegrinder/client/abc.py
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
+
import io
|
|
1
2
|
import typing
|
|
2
3
|
from abc import ABC, abstractmethod
|
|
3
4
|
|
|
5
|
+
from telegrinder.client.form_data import MultipartFormProto, encode_form_data
|
|
6
|
+
|
|
7
|
+
type Data = dict[str, typing.Any] | MultipartFormProto
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ABCClient[MultipartForm: MultipartFormProto](ABC):
|
|
11
|
+
CONNECTION_TIMEOUT_ERRORS: tuple[type[BaseException], ...] = ()
|
|
12
|
+
CLIENT_CONNECTION_ERRORS: tuple[type[BaseException], ...] = ()
|
|
4
13
|
|
|
5
|
-
class ABCClient(ABC):
|
|
6
14
|
@abstractmethod
|
|
7
|
-
def __init__(self, *args: typing.Any, **kwargs: typing.Any):
|
|
15
|
+
def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
|
|
8
16
|
pass
|
|
9
17
|
|
|
10
18
|
@abstractmethod
|
|
@@ -12,7 +20,7 @@ class ABCClient(ABC):
|
|
|
12
20
|
self,
|
|
13
21
|
url: str,
|
|
14
22
|
method: str = "GET",
|
|
15
|
-
data:
|
|
23
|
+
data: Data | None = None,
|
|
16
24
|
**kwargs: typing.Any,
|
|
17
25
|
) -> str:
|
|
18
26
|
pass
|
|
@@ -22,7 +30,7 @@ class ABCClient(ABC):
|
|
|
22
30
|
self,
|
|
23
31
|
url: str,
|
|
24
32
|
method: str = "GET",
|
|
25
|
-
data:
|
|
33
|
+
data: Data | None = None,
|
|
26
34
|
**kwargs: typing.Any,
|
|
27
35
|
) -> dict[str, typing.Any]:
|
|
28
36
|
pass
|
|
@@ -32,7 +40,7 @@ class ABCClient(ABC):
|
|
|
32
40
|
self,
|
|
33
41
|
url: str,
|
|
34
42
|
method: str = "GET",
|
|
35
|
-
data:
|
|
43
|
+
data: Data | None = None,
|
|
36
44
|
**kwargs: typing.Any,
|
|
37
45
|
) -> bytes:
|
|
38
46
|
pass
|
|
@@ -42,7 +50,7 @@ class ABCClient(ABC):
|
|
|
42
50
|
self,
|
|
43
51
|
url: str,
|
|
44
52
|
method: str = "GET",
|
|
45
|
-
data:
|
|
53
|
+
data: Data | None = None,
|
|
46
54
|
**kwargs: typing.Any,
|
|
47
55
|
) -> bytes:
|
|
48
56
|
pass
|
|
@@ -53,12 +61,28 @@ class ABCClient(ABC):
|
|
|
53
61
|
|
|
54
62
|
@classmethod
|
|
55
63
|
@abstractmethod
|
|
64
|
+
def multipart_form_factory(cls) -> MultipartForm:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
56
68
|
def get_form(
|
|
57
69
|
cls,
|
|
70
|
+
*,
|
|
58
71
|
data: dict[str, typing.Any],
|
|
59
|
-
files: dict[str, tuple[str,
|
|
60
|
-
) ->
|
|
61
|
-
|
|
72
|
+
files: dict[str, tuple[str, typing.Any]] | None = None,
|
|
73
|
+
) -> MultipartForm:
|
|
74
|
+
multipart_form = cls.multipart_form_factory()
|
|
75
|
+
files = files or {}
|
|
76
|
+
|
|
77
|
+
for k, v in encode_form_data(data, files).items():
|
|
78
|
+
multipart_form.add_field(k, v)
|
|
79
|
+
|
|
80
|
+
for n, (filename, content) in {
|
|
81
|
+
k: (n, io.BytesIO(c) if isinstance(c, bytes) else c) for k, (n, c) in files.items()
|
|
82
|
+
}.items():
|
|
83
|
+
multipart_form.add_field(n, content, filename=filename)
|
|
84
|
+
|
|
85
|
+
return multipart_form
|
|
62
86
|
|
|
63
87
|
async def __aenter__(self) -> typing.Self:
|
|
64
88
|
return self
|
|
@@ -68,8 +92,9 @@ class ABCClient(ABC):
|
|
|
68
92
|
exc_type: type[BaseException],
|
|
69
93
|
exc_val: typing.Any,
|
|
70
94
|
exc_tb: typing.Any,
|
|
71
|
-
) ->
|
|
95
|
+
) -> bool:
|
|
72
96
|
await self.close()
|
|
97
|
+
return not bool(exc_val)
|
|
73
98
|
|
|
74
99
|
|
|
75
100
|
__all__ = ("ABCClient",)
|
telegrinder/client/aiohttp.py
CHANGED
|
@@ -11,8 +11,23 @@ from telegrinder.client.abc import ABCClient
|
|
|
11
11
|
if typing.TYPE_CHECKING:
|
|
12
12
|
from aiohttp import ClientResponse
|
|
13
13
|
|
|
14
|
+
type Data = dict[str, typing.Any] | aiohttp.formdata.FormData
|
|
15
|
+
type Response = ClientResponse
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AiohttpClient(ABCClient[aiohttp.formdata.FormData]):
|
|
19
|
+
"""HTTP client based on `aiohttp` module."""
|
|
20
|
+
|
|
21
|
+
CONNECTION_TIMEOUT_ERRORS = (
|
|
22
|
+
aiohttp.client.ServerConnectionError,
|
|
23
|
+
TimeoutError,
|
|
24
|
+
)
|
|
25
|
+
CLIENT_CONNECTION_ERRORS = (
|
|
26
|
+
aiohttp.client.ClientConnectionError,
|
|
27
|
+
aiohttp.client.ClientConnectorError,
|
|
28
|
+
aiohttp.ClientOSError,
|
|
29
|
+
)
|
|
14
30
|
|
|
15
|
-
class AiohttpClient(ABCClient):
|
|
16
31
|
def __init__(
|
|
17
32
|
self,
|
|
18
33
|
session: ClientSession | None = None,
|
|
@@ -35,15 +50,16 @@ class AiohttpClient(ABCClient):
|
|
|
35
50
|
self,
|
|
36
51
|
url: str,
|
|
37
52
|
method: str = "GET",
|
|
38
|
-
data:
|
|
53
|
+
data: Data | None = None,
|
|
39
54
|
**kwargs: typing.Any,
|
|
40
|
-
) ->
|
|
55
|
+
) -> Response:
|
|
41
56
|
if not self.session:
|
|
42
57
|
self.session = ClientSession(
|
|
43
58
|
connector=TCPConnector(ssl=ssl.create_default_context(cafile=certifi.where())),
|
|
44
59
|
json_serialize=json.dumps,
|
|
45
60
|
**self.session_params,
|
|
46
61
|
)
|
|
62
|
+
|
|
47
63
|
async with self.session.request(
|
|
48
64
|
url=url,
|
|
49
65
|
method=method,
|
|
@@ -58,7 +74,7 @@ class AiohttpClient(ABCClient):
|
|
|
58
74
|
self,
|
|
59
75
|
url: str,
|
|
60
76
|
method: str = "GET",
|
|
61
|
-
data:
|
|
77
|
+
data: Data | None = None,
|
|
62
78
|
**kwargs: typing.Any,
|
|
63
79
|
) -> dict[str, typing.Any]:
|
|
64
80
|
response = await self.request_raw(url, method, data, **kwargs)
|
|
@@ -72,7 +88,7 @@ class AiohttpClient(ABCClient):
|
|
|
72
88
|
self,
|
|
73
89
|
url: str,
|
|
74
90
|
method: str = "GET",
|
|
75
|
-
data:
|
|
91
|
+
data: Data | None = None,
|
|
76
92
|
**kwargs: typing.Any,
|
|
77
93
|
) -> str:
|
|
78
94
|
response = await self.request_raw(url, method, data, **kwargs) # type: ignore
|
|
@@ -82,48 +98,36 @@ class AiohttpClient(ABCClient):
|
|
|
82
98
|
self,
|
|
83
99
|
url: str,
|
|
84
100
|
method: str = "GET",
|
|
85
|
-
data:
|
|
101
|
+
data: Data | None = None,
|
|
86
102
|
**kwargs: typing.Any,
|
|
87
103
|
) -> bytes:
|
|
88
104
|
response = await self.request_raw(url, method, data, **kwargs) # type: ignore
|
|
89
105
|
if response._body is None:
|
|
90
106
|
await response.read()
|
|
91
|
-
return response._body
|
|
107
|
+
return response._body or bytes()
|
|
92
108
|
|
|
93
109
|
async def request_content(
|
|
94
110
|
self,
|
|
95
111
|
url: str,
|
|
96
112
|
method: str = "GET",
|
|
97
|
-
data:
|
|
113
|
+
data: Data | None = None,
|
|
98
114
|
**kwargs: typing.Any,
|
|
99
115
|
) -> bytes:
|
|
100
116
|
response = await self.request_raw(url, method, data, **kwargs)
|
|
101
|
-
return response._body
|
|
117
|
+
return response._body or bytes()
|
|
102
118
|
|
|
103
119
|
async def close(self) -> None:
|
|
104
120
|
if self.session and not self.session.closed:
|
|
105
121
|
await self.session.close()
|
|
106
122
|
|
|
107
123
|
@classmethod
|
|
108
|
-
def
|
|
109
|
-
|
|
110
|
-
data: dict[str, typing.Any],
|
|
111
|
-
files: dict[str, tuple[str, bytes]] | None = None,
|
|
112
|
-
) -> aiohttp.formdata.FormData:
|
|
113
|
-
files = files or {}
|
|
114
|
-
form = aiohttp.formdata.FormData(quote_fields=False)
|
|
115
|
-
for k, v in data.items():
|
|
116
|
-
form.add_field(k, str(v))
|
|
117
|
-
|
|
118
|
-
for n, f in files.items():
|
|
119
|
-
form.add_field(n, f[1], filename=f[0])
|
|
120
|
-
|
|
121
|
-
return form
|
|
124
|
+
def multipart_form_factory(cls) -> aiohttp.formdata.FormData:
|
|
125
|
+
return aiohttp.formdata.FormData(quote_fields=False)
|
|
122
126
|
|
|
123
127
|
def __del__(self) -> None:
|
|
124
128
|
if self.session and not self.session.closed:
|
|
125
129
|
if self.session._connector is not None and self.session._connector_owner:
|
|
126
|
-
self.session._connector.
|
|
130
|
+
self.session._connector._close()
|
|
127
131
|
self.session._connector = None
|
|
128
132
|
|
|
129
133
|
|