telegrinder 0.1.dev20__py3-none-any.whl → 0.1.dev159__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of telegrinder might be problematic. Click here for more details.

Files changed (132) hide show
  1. telegrinder/__init__.py +129 -22
  2. telegrinder/api/__init__.py +11 -2
  3. telegrinder/api/abc.py +25 -9
  4. telegrinder/api/api.py +47 -28
  5. telegrinder/api/error.py +14 -4
  6. telegrinder/api/response.py +11 -7
  7. telegrinder/bot/__init__.py +68 -7
  8. telegrinder/bot/bot.py +30 -24
  9. telegrinder/bot/cute_types/__init__.py +11 -1
  10. telegrinder/bot/cute_types/base.py +138 -0
  11. telegrinder/bot/cute_types/callback_query.py +458 -15
  12. telegrinder/bot/cute_types/inline_query.py +30 -24
  13. telegrinder/bot/cute_types/message.py +2982 -78
  14. telegrinder/bot/cute_types/update.py +30 -0
  15. telegrinder/bot/cute_types/utils.py +794 -0
  16. telegrinder/bot/dispatch/__init__.py +56 -3
  17. telegrinder/bot/dispatch/abc.py +9 -7
  18. telegrinder/bot/dispatch/composition.py +74 -0
  19. telegrinder/bot/dispatch/context.py +71 -0
  20. telegrinder/bot/dispatch/dispatch.py +86 -49
  21. telegrinder/bot/dispatch/handler/__init__.py +3 -0
  22. telegrinder/bot/dispatch/handler/abc.py +11 -5
  23. telegrinder/bot/dispatch/handler/func.py +41 -32
  24. telegrinder/bot/dispatch/handler/message_reply.py +46 -0
  25. telegrinder/bot/dispatch/middleware/__init__.py +2 -0
  26. telegrinder/bot/dispatch/middleware/abc.py +10 -4
  27. telegrinder/bot/dispatch/process.py +53 -49
  28. telegrinder/bot/dispatch/return_manager/__init__.py +19 -0
  29. telegrinder/bot/dispatch/return_manager/abc.py +95 -0
  30. telegrinder/bot/dispatch/return_manager/callback_query.py +19 -0
  31. telegrinder/bot/dispatch/return_manager/inline_query.py +14 -0
  32. telegrinder/bot/dispatch/return_manager/message.py +25 -0
  33. telegrinder/bot/dispatch/view/__init__.py +14 -2
  34. telegrinder/bot/dispatch/view/abc.py +128 -2
  35. telegrinder/bot/dispatch/view/box.py +38 -0
  36. telegrinder/bot/dispatch/view/callback_query.py +13 -39
  37. telegrinder/bot/dispatch/view/inline_query.py +11 -39
  38. telegrinder/bot/dispatch/view/message.py +11 -47
  39. telegrinder/bot/dispatch/waiter_machine/__init__.py +9 -0
  40. telegrinder/bot/dispatch/waiter_machine/machine.py +116 -0
  41. telegrinder/bot/dispatch/waiter_machine/middleware.py +76 -0
  42. telegrinder/bot/dispatch/waiter_machine/short_state.py +37 -0
  43. telegrinder/bot/polling/__init__.py +2 -0
  44. telegrinder/bot/polling/abc.py +11 -4
  45. telegrinder/bot/polling/polling.py +89 -40
  46. telegrinder/bot/rules/__init__.py +91 -5
  47. telegrinder/bot/rules/abc.py +81 -63
  48. telegrinder/bot/rules/adapter/__init__.py +11 -0
  49. telegrinder/bot/rules/adapter/abc.py +21 -0
  50. telegrinder/bot/rules/adapter/errors.py +5 -0
  51. telegrinder/bot/rules/adapter/event.py +49 -0
  52. telegrinder/bot/rules/adapter/raw_update.py +24 -0
  53. telegrinder/bot/rules/callback_data.py +159 -38
  54. telegrinder/bot/rules/command.py +116 -0
  55. telegrinder/bot/rules/enum_text.py +28 -0
  56. telegrinder/bot/rules/func.py +17 -17
  57. telegrinder/bot/rules/fuzzy.py +13 -10
  58. telegrinder/bot/rules/inline.py +61 -0
  59. telegrinder/bot/rules/integer.py +12 -7
  60. telegrinder/bot/rules/is_from.py +148 -7
  61. telegrinder/bot/rules/markup.py +21 -18
  62. telegrinder/bot/rules/mention.py +17 -0
  63. telegrinder/bot/rules/message_entities.py +33 -0
  64. telegrinder/bot/rules/regex.py +27 -19
  65. telegrinder/bot/rules/rule_enum.py +74 -0
  66. telegrinder/bot/rules/start.py +25 -13
  67. telegrinder/bot/rules/text.py +23 -14
  68. telegrinder/bot/scenario/__init__.py +2 -0
  69. telegrinder/bot/scenario/abc.py +12 -5
  70. telegrinder/bot/scenario/checkbox.py +48 -30
  71. telegrinder/bot/scenario/choice.py +16 -10
  72. telegrinder/client/__init__.py +3 -1
  73. telegrinder/client/abc.py +26 -16
  74. telegrinder/client/aiohttp.py +54 -32
  75. telegrinder/model.py +119 -40
  76. telegrinder/modules.py +189 -21
  77. telegrinder/msgspec_json.py +14 -0
  78. telegrinder/msgspec_utils.py +227 -0
  79. telegrinder/node/__init__.py +31 -0
  80. telegrinder/node/attachment.py +71 -0
  81. telegrinder/node/base.py +93 -0
  82. telegrinder/node/composer.py +71 -0
  83. telegrinder/node/container.py +22 -0
  84. telegrinder/node/message.py +18 -0
  85. telegrinder/node/rule.py +56 -0
  86. telegrinder/node/source.py +31 -0
  87. telegrinder/node/text.py +13 -0
  88. telegrinder/node/tools/__init__.py +3 -0
  89. telegrinder/node/tools/generator.py +40 -0
  90. telegrinder/node/update.py +12 -0
  91. telegrinder/rules.py +1 -1
  92. telegrinder/tools/__init__.py +138 -4
  93. telegrinder/tools/buttons.py +89 -51
  94. telegrinder/tools/error_handler/__init__.py +8 -0
  95. telegrinder/tools/error_handler/abc.py +30 -0
  96. telegrinder/tools/error_handler/error_handler.py +156 -0
  97. telegrinder/tools/formatting/__init__.py +81 -3
  98. telegrinder/tools/formatting/html.py +283 -37
  99. telegrinder/tools/formatting/links.py +32 -0
  100. telegrinder/tools/formatting/spec_html_formats.py +121 -0
  101. telegrinder/tools/global_context/__init__.py +12 -0
  102. telegrinder/tools/global_context/abc.py +66 -0
  103. telegrinder/tools/global_context/global_context.py +451 -0
  104. telegrinder/tools/global_context/telegrinder_ctx.py +25 -0
  105. telegrinder/tools/i18n/__init__.py +12 -0
  106. telegrinder/tools/i18n/base.py +31 -0
  107. telegrinder/tools/i18n/middleware/__init__.py +3 -0
  108. telegrinder/tools/i18n/middleware/base.py +26 -0
  109. telegrinder/tools/i18n/simple.py +48 -0
  110. telegrinder/tools/kb_set/__init__.py +2 -0
  111. telegrinder/tools/kb_set/base.py +3 -0
  112. telegrinder/tools/kb_set/yaml.py +28 -17
  113. telegrinder/tools/keyboard.py +84 -62
  114. telegrinder/tools/loop_wrapper/__init__.py +4 -0
  115. telegrinder/tools/loop_wrapper/abc.py +18 -0
  116. telegrinder/tools/loop_wrapper/loop_wrapper.py +132 -0
  117. telegrinder/tools/magic.py +48 -23
  118. telegrinder/tools/parse_mode.py +1 -2
  119. telegrinder/types/__init__.py +1 -0
  120. telegrinder/types/enums.py +653 -0
  121. telegrinder/types/methods.py +4107 -1279
  122. telegrinder/types/objects.py +4771 -1745
  123. {telegrinder-0.1.dev20.dist-info → telegrinder-0.1.dev159.dist-info}/LICENSE +2 -1
  124. telegrinder-0.1.dev159.dist-info/METADATA +109 -0
  125. telegrinder-0.1.dev159.dist-info/RECORD +126 -0
  126. {telegrinder-0.1.dev20.dist-info → telegrinder-0.1.dev159.dist-info}/WHEEL +1 -1
  127. telegrinder/bot/dispatch/waiter.py +0 -38
  128. telegrinder/result.py +0 -38
  129. telegrinder/tools/formatting/abc.py +0 -52
  130. telegrinder/tools/formatting/markdown.py +0 -57
  131. telegrinder-0.1.dev20.dist-info/METADATA +0 -22
  132. telegrinder-0.1.dev20.dist-info/RECORD +0 -71
@@ -1,102 +1,120 @@
1
- from abc import ABC, abstractmethod
2
- from telegrinder.bot.cute_types import MessageCute
3
- from telegrinder.types import Update
4
- import typing
5
- import collections
6
1
  import inspect
7
- import vbml
2
+ import typing
3
+ from abc import ABC, abstractmethod
8
4
 
9
- T = typing.TypeVar("T")
10
- patcher = vbml.Patcher()
5
+ from telegrinder.bot.cute_types import BaseCute, MessageCute, UpdateCute
6
+ from telegrinder.bot.dispatch.context import Context
7
+ from telegrinder.bot.dispatch.process import check_rule
8
+ from telegrinder.bot.rules.adapter import ABCAdapter, EventAdapter, RawUpdateAdapter
9
+ from telegrinder.tools.i18n.base import ABCTranslator
10
+ from telegrinder.tools.magic import cache_translation, get_cached_translation
11
+ from telegrinder.types.objects import Update as UpdateObject
11
12
 
12
- Message = MessageCute
13
- EventScheme = collections.namedtuple("EventScheme", ["name", "dataclass"])
13
+ T = typing.TypeVar("T", bound=BaseCute)
14
14
 
15
+ Message: typing.TypeAlias = MessageCute
16
+ Update: typing.TypeAlias = UpdateCute
15
17
 
16
- class ABCRule(ABC, typing.Generic[T]):
17
- __event__: typing.Optional[EventScheme] = None
18
- require: typing.List["ABCRule[T]"] = []
19
18
 
20
- async def run_check(self, event: T, ctx: dict) -> bool:
21
- ctx_copy = ctx.copy()
22
- for required in self.require:
23
- if not await required.run_check(event, ctx_copy):
24
- return False
25
- ctx.update(ctx_copy)
26
- return await self.check(event, ctx)
19
+ def with_caching_translations(func):
20
+ """Should be used as decorator for .translate method. Caches rule translations."""
21
+
22
+ async def wrapper(self: "ABCRule", translator: ABCTranslator):
23
+ if translation := get_cached_translation(self, translator.locale):
24
+ return translation
25
+ translation = await func(self, translator)
26
+ cache_translation(self, translator.locale, translation)
27
+ return translation
28
+
29
+ return wrapper
30
+
31
+
32
+ class ABCRule(ABC, typing.Generic[T]):
33
+ adapter: ABCAdapter[UpdateObject, T] = RawUpdateAdapter() # type: ignore
34
+ requires: list["ABCRule[T]"] = []
27
35
 
28
36
  @abstractmethod
29
- async def check(self, event: T, ctx: dict) -> bool:
37
+ async def check(self, event: T, ctx: Context) -> bool:
30
38
  pass
31
39
 
32
- def __init_subclass__(cls, require: typing.Optional[typing.List["ABCRule[T]"]] = None):
33
- """Merges requirements from inherited classes and rule-specific requirements"""
40
+ def __init_subclass__(cls, requires: list["ABCRule[T]"] | None = None):
41
+ """Merges requirements from inherited classes and rule-specific requirements."""
42
+
34
43
  requirements = []
35
44
  for base in inspect.getmro(cls):
36
45
  if issubclass(base, ABCRule) and base != cls:
37
- requirements.extend(base.require or ())
46
+ requirements.extend(base.requires or ()) # type: ignore
38
47
 
39
- requirements.extend(require or ())
40
- cls.require = list(dict.fromkeys(requirements))
48
+ requirements.extend(requires or ())
49
+ cls.requires = list(dict.fromkeys(requirements))
41
50
 
42
- def __and__(self, other: "ABCRule"):
51
+ def __and__(self, other: "ABCRule[T]"):
43
52
  return AndRule(self, other)
44
53
 
45
- def __or__(self, other: "ABCRule"):
54
+ def __or__(self, other: "ABCRule[T]"):
46
55
  return OrRule(self, other)
47
56
 
57
+ def __neg__(self) -> "ABCRule[T]":
58
+ return NotRule(self)
59
+
48
60
  def __repr__(self) -> str:
49
- return f"(rule {self.__class__.__name__})"
61
+ return "<rule: {!r}, adapter: {!r}>".format(
62
+ self.__class__.__name__,
63
+ self.adapter,
64
+ )
50
65
 
66
+ async def translate(self, translator: ABCTranslator) -> typing.Self:
67
+ return self
51
68
 
52
- class AndRule(ABCRule):
53
- def __init__(self, *rules: ABCRule):
69
+
70
+ class AndRule(ABCRule[T]):
71
+ def __init__(self, *rules: ABCRule[T]):
54
72
  self.rules = rules
55
73
 
56
- async def check(self, event: Update, ctx: dict) -> bool:
74
+ async def check(self, event: Update, ctx: Context) -> bool:
57
75
  ctx_copy = ctx.copy()
58
76
  for rule in self.rules:
59
- e = event
60
- if rule.__event__:
61
- event_dict = event.to_dict()
62
- if rule.__event__.name not in event:
63
- return False
64
- e = rule.__event__.dataclass(
65
- **event_dict[rule.__event__.name].to_dict()
66
- )
67
- if not await rule.run_check(e, ctx_copy):
77
+ if not await check_rule(event.ctx_api, rule, event, ctx_copy):
68
78
  return False
69
- ctx.clear()
70
- ctx.update(ctx_copy)
79
+ ctx |= ctx_copy
71
80
  return True
72
81
 
73
82
 
74
- class OrRule(ABCRule):
75
- def __init__(self, *rules: ABCRule):
83
+ class OrRule(ABCRule[T]):
84
+ def __init__(self, *rules: ABCRule[T]):
76
85
  self.rules = rules
77
86
 
78
- async def check(self, event: T, ctx: dict) -> bool:
79
- ctx_copy = ctx.copy()
80
- found = False
81
-
87
+ async def check(self, event: Update, ctx: Context) -> bool:
82
88
  for rule in self.rules:
83
- e = event
84
- if rule.__event__:
85
- if rule.__event__.name not in event:
86
- continue
87
- e = rule.__event__.dataclass(**event[rule.__event__.name])
88
- if await rule.run_check(e, ctx_copy):
89
- found = True
90
- break
89
+ ctx_copy = ctx.copy()
90
+ if await check_rule(event.ctx_api, rule, event, ctx_copy):
91
+ ctx |= ctx_copy
92
+ return True
93
+ return False
91
94
 
92
- ctx.clear()
93
- ctx.update(ctx_copy)
94
- return found
95
95
 
96
+ class NotRule(ABCRule[T]):
97
+ def __init__(self, rule: ABCRule[T]):
98
+ self.rule = rule
96
99
 
97
- class ABCMessageRule(ABCRule, ABC, require=[]):
98
- __event__ = EventScheme("message", Message)
100
+ async def check(self, event: Update, ctx: Context) -> bool:
101
+ ctx_copy = ctx.copy()
102
+ return not await check_rule(event.ctx_api, self.rule, event, ctx_copy)
103
+
104
+
105
+ class MessageRule(ABCRule[Message], ABC, requires=[]):
106
+ adapter = EventAdapter("message", Message)
99
107
 
100
108
  @abstractmethod
101
- async def check(self, message: Message, ctx: dict) -> bool:
109
+ async def check(self, message: Message, ctx: Context) -> bool:
102
110
  ...
111
+
112
+
113
+ __all__ = (
114
+ "ABCRule",
115
+ "AndRule",
116
+ "MessageRule",
117
+ "NotRule",
118
+ "OrRule",
119
+ "with_caching_translations",
120
+ )
@@ -0,0 +1,11 @@
1
+ from .abc import ABCAdapter
2
+ from .errors import AdapterError
3
+ from .event import EventAdapter
4
+ from .raw_update import RawUpdateAdapter
5
+
6
+ __all__ = (
7
+ "ABCAdapter",
8
+ "AdapterError",
9
+ "EventAdapter",
10
+ "RawUpdateAdapter",
11
+ )
@@ -0,0 +1,21 @@
1
+ import abc
2
+ import typing
3
+
4
+ from fntypes.result import Result
5
+
6
+ from telegrinder.api.abc import ABCAPI
7
+ from telegrinder.bot.cute_types import BaseCute
8
+ from telegrinder.bot.rules.adapter.errors import AdapterError
9
+ from telegrinder.model import Model
10
+
11
+ UpdateT = typing.TypeVar("UpdateT", bound=Model)
12
+ CuteT = typing.TypeVar("CuteT", bound=BaseCute)
13
+
14
+
15
+ class ABCAdapter(abc.ABC, typing.Generic[UpdateT, CuteT]):
16
+ @abc.abstractmethod
17
+ async def adapt(self, api: ABCAPI, update: UpdateT) -> Result[CuteT, AdapterError]:
18
+ pass
19
+
20
+
21
+ __all__ = ("ABCAdapter",)
@@ -0,0 +1,5 @@
1
+ class AdapterError(RuntimeError):
2
+ pass
3
+
4
+
5
+ __all__ = ("AdapterError",)
@@ -0,0 +1,49 @@
1
+ import typing
2
+
3
+ from fntypes.result import Error, Ok, Result
4
+
5
+ from telegrinder.api.abc import ABCAPI
6
+ from telegrinder.bot.cute_types import BaseCute
7
+ from telegrinder.bot.rules.adapter.abc import ABCAdapter
8
+ from telegrinder.bot.rules.adapter.errors import AdapterError
9
+ from telegrinder.msgspec_utils import Nothing
10
+ from telegrinder.types.objects import Model, Update
11
+
12
+ EventT = typing.TypeVar("EventT", bound=Model)
13
+ CuteT = typing.TypeVar("CuteT", bound=BaseCute)
14
+
15
+
16
+ class EventAdapter(ABCAdapter[Update, CuteT]):
17
+ def __init__(self, event_name: str, model: type[CuteT]) -> None:
18
+ self.event_name = event_name
19
+ self.model = model
20
+
21
+ def __repr__(self) -> str:
22
+ raw_update_type = Update.__annotations__.get(self.event_name, "Unknown")
23
+ raw_update_type = (
24
+ typing.get_args(raw_update_type)[0].__forward_arg__
25
+ if typing.get_args(raw_update_type)
26
+ else raw_update_type
27
+ )
28
+ return "<{}: adapt {} -> {}>".format(
29
+ self.__class__.__name__,
30
+ raw_update_type,
31
+ self.model.__name__,
32
+ )
33
+
34
+ async def adapt(self, api: ABCAPI, update: Update) -> Result[CuteT, AdapterError]:
35
+ update_dct = update.to_dict()
36
+ if self.event_name not in update_dct:
37
+ return Error(
38
+ AdapterError(f"Update is not of event type {self.event_name!r}."),
39
+ )
40
+ if update_dct[self.event_name] is Nothing:
41
+ return Error(
42
+ AdapterError(f"Update is not an {self.event_name!r}."),
43
+ )
44
+ return Ok(
45
+ self.model.from_update(update_dct[self.event_name].unwrap(), bound_api=api),
46
+ )
47
+
48
+
49
+ __all__ = ("EventAdapter",)
@@ -0,0 +1,24 @@
1
+ from fntypes.result import Ok, Result
2
+
3
+ from telegrinder.api.abc import ABCAPI
4
+ from telegrinder.bot.cute_types.update import UpdateCute
5
+ from telegrinder.bot.rules.adapter.abc import ABCAdapter
6
+ from telegrinder.bot.rules.adapter.errors import AdapterError
7
+ from telegrinder.types.objects import Update
8
+
9
+
10
+ class RawUpdateAdapter(ABCAdapter[Update, UpdateCute]):
11
+ def __repr__(self) -> str:
12
+ return f"<{self.__class__.__name__}: adapt Update -> UpdateCute>"
13
+
14
+ async def adapt(
15
+ self,
16
+ api: ABCAPI,
17
+ update: Update,
18
+ ) -> Result[UpdateCute, AdapterError]:
19
+ if not isinstance(update, UpdateCute):
20
+ return Ok(UpdateCute.from_update(update, api))
21
+ return Ok(update)
22
+
23
+
24
+ __all__ = ("RawUpdateAdapter",)
@@ -1,57 +1,178 @@
1
- from .abc import ABCRule, EventScheme
2
- from telegrinder.modules import json
3
- from telegrinder.types import Update
4
- from telegrinder.bot.cute_types import CallbackQueryCute
5
- from .markup import Markup, check_string
6
- import msgspec
7
- import vbml
1
+ import abc
2
+ import inspect
8
3
  import typing
4
+ from contextlib import suppress
5
+
6
+ import msgspec
7
+
8
+ from telegrinder.bot.cute_types import CallbackQueryCute
9
+ from telegrinder.bot.dispatch.context import Context
10
+ from telegrinder.bot.rules.adapter import EventAdapter
11
+ from telegrinder.model import decoder
12
+ from telegrinder.tools.buttons import DataclassInstance
13
+
14
+ from .abc import ABCRule
15
+ from .markup import Markup, PatternLike, check_string
16
+
17
+ if typing.TYPE_CHECKING:
18
+
19
+ T = typing.TypeVar("T")
20
+ Ref: typing.TypeAlias = typing.Annotated[T, ...]
21
+ else:
22
+
23
+ class Ref:
24
+ def __class_getitem__(cls, code: str) -> typing.ForwardRef:
25
+ return typing.ForwardRef(code)
26
+
9
27
 
10
28
  CallbackQuery = CallbackQueryCute
11
- PatternLike = typing.Union[str, vbml.Pattern]
29
+ Validator: typing.TypeAlias = typing.Callable[[typing.Any], bool | typing.Awaitable[bool]]
30
+ MapDict: typing.TypeAlias = dict[
31
+ str, typing.Any | type[typing.Any] | Validator | list[Ref["MapDict"]] | Ref["MapDict"]
32
+ ]
33
+ CallbackMap: typing.TypeAlias = list[tuple[str, typing.Any | type | Validator | Ref["CallbackMap"]]]
34
+ CallbackMapStrict: typing.TypeAlias = list[tuple[str, Validator | Ref["CallbackMapStrict"]]]
12
35
 
13
36
 
14
- class CallbackDataEq(ABCRule):
15
- def __init__(self, value: str):
16
- self.value = value
37
+ class CallbackQueryRule(ABCRule[CallbackQuery], abc.ABC):
38
+ adapter = EventAdapter("callback_query", CallbackQuery)
17
39
 
18
- async def check(self, event: Update, ctx: dict) -> bool:
19
- return event.callback_query.data == self.value
40
+ @abc.abstractmethod
41
+ async def check(self, event: CallbackQuery, ctx: Context) -> bool:
42
+ pass
20
43
 
21
44
 
22
- class CallbackDataJsonEq(ABCRule):
23
- def __init__(self, d: dict):
24
- self.d = d
45
+ class HasData(CallbackQueryRule):
46
+ async def check(self, event: CallbackQuery, ctx: Context) -> bool:
47
+ return bool(event.data or event.data.unwrap())
25
48
 
26
- async def check(self, event: Update, ctx: dict) -> bool:
27
- if not event.callback_query.data:
28
- return False
29
- try:
30
- # todo: use msgspec
31
- return json.loads(event.callback_query.data) == self.d
32
- except:
33
- return False
34
49
 
50
+ class CallbackQueryDataRule(CallbackQueryRule, abc.ABC, requires=[HasData()]):
51
+ pass
35
52
 
36
- class CallbackDataJsonModel(ABCRule[CallbackQuery]):
37
- __event__ = EventScheme("callback_query", CallbackQuery)
38
53
 
39
- def __init__(self, model: typing.Type[msgspec.Struct]):
40
- self.decoder = msgspec.json.Decoder(type=model)
54
+ class CallbackDataMap(CallbackQueryDataRule):
55
+ def __init__(self, mapping: MapDict) -> None:
56
+ self.mapping = self.transform_to_callbacks(
57
+ self.transform_to_map(mapping),
58
+ )
41
59
 
42
- async def check(self, event: CallbackQuery, ctx: dict) -> bool:
43
- try:
44
- ctx["data"] = self.decoder.decode(event.data.encode())
45
- return True
46
- except msgspec.DecodeError:
60
+ @classmethod
61
+ def transform_to_map(cls, mapping: MapDict) -> CallbackMap:
62
+ """Transforms MapDict to CallbackMap."""
63
+
64
+ callback_map = []
65
+
66
+ for k, v in mapping.items():
67
+ if isinstance(v, dict):
68
+ v = cls.transform_to_map(v)
69
+ callback_map.append((k, v))
70
+
71
+ return callback_map
72
+
73
+ @classmethod
74
+ def transform_to_callbacks(cls, callback_map: CallbackMap) -> CallbackMapStrict:
75
+ """Transforms `CallbackMap` to `CallbackMapStrict`."""
76
+
77
+ callback_map_result = []
78
+
79
+ for key, value in callback_map:
80
+ if isinstance(value, type):
81
+ validator = (lambda tp: lambda v: isinstance(v, tp))(value)
82
+ elif isinstance(value, list):
83
+ validator = cls.transform_to_callbacks(value)
84
+ elif not callable(value):
85
+ validator = (lambda val: lambda v: val == v)(value)
86
+ else:
87
+ validator = value
88
+ callback_map_result.append((key, validator))
89
+
90
+ return callback_map_result
91
+
92
+ @staticmethod
93
+ async def run_validator(value: typing.Any, validator: Validator) -> bool:
94
+ """Run async or sync validator."""
95
+
96
+ with suppress(BaseException):
97
+ result = validator(value)
98
+ if inspect.isawaitable(result):
99
+ result = await result
100
+ return result # type: ignore
101
+
102
+ return False
103
+
104
+ @classmethod
105
+ async def match(cls, callback_data: dict, callback_map: CallbackMapStrict) -> bool:
106
+ """Matches callback_data with callback_map recursively."""
107
+
108
+ for key, validator in callback_map:
109
+ if key not in callback_data:
110
+ return False
111
+
112
+ if isinstance(validator, list):
113
+ if not (
114
+ isinstance(callback_data[key], dict)
115
+ and await cls.match(callback_data[key], validator)
116
+ ):
117
+ return False
118
+
119
+ elif not await cls.run_validator(callback_data[key], validator):
120
+ return False
121
+
122
+ return True
123
+
124
+ async def check(self, event: CallbackQuery, ctx: Context) -> bool:
125
+ callback_data = event.decode_callback_data().unwrap_or_none()
126
+ if callback_data is None:
47
127
  return False
128
+ if await self.match(callback_data, self.mapping):
129
+ ctx.update(callback_data)
130
+ return True
131
+ return False
132
+
133
+
134
+ class CallbackDataEq(CallbackQueryDataRule):
135
+ def __init__(self, value: str):
136
+ self.value = value
48
137
 
138
+ async def check(self, event: CallbackQuery, ctx: Context) -> bool:
139
+ return event.data.unwrap() == self.value
49
140
 
50
- class CallbackDataMarkup(ABCRule[CallbackQuery]):
51
- __event__ = EventScheme("callback_query", CallbackQuery)
52
141
 
53
- def __init__(self, patterns: typing.Union[PatternLike, typing.List[PatternLike]]):
142
+ class CallbackDataJsonEq(CallbackQueryDataRule):
143
+ def __init__(self, d: dict[str, typing.Any]):
144
+ self.d = d
145
+
146
+ async def check(self, event: CallbackQuery, ctx: Context) -> bool:
147
+ return event.decode_callback_data().unwrap_or_none() == self.d
148
+
149
+
150
+ class CallbackDataJsonModel(CallbackQueryDataRule):
151
+ def __init__(self, model: type[msgspec.Struct] | type[DataclassInstance]):
152
+ self.model = model
153
+
154
+ async def check(self, event: CallbackQuery, ctx: Context) -> bool:
155
+ with suppress(BaseException):
156
+ ctx.data = decoder.decode(event.data.unwrap().encode(), type=self.model)
157
+ return True
158
+ return False
159
+
160
+
161
+ class CallbackDataMarkup(CallbackQueryDataRule):
162
+ def __init__(self, patterns: PatternLike | list[PatternLike]):
54
163
  self.patterns = Markup(patterns).patterns
55
164
 
56
- async def check(self, event: CallbackQuery, ctx: dict) -> bool:
57
- return check_string(self.patterns, event.data, ctx)
165
+ async def check(self, event: CallbackQuery, ctx: Context) -> bool:
166
+ return check_string(self.patterns, event.data.unwrap(), ctx)
167
+
168
+
169
+ __all__ = (
170
+ "CallbackDataEq",
171
+ "CallbackDataJsonEq",
172
+ "CallbackDataJsonModel",
173
+ "CallbackDataMap",
174
+ "CallbackDataMarkup",
175
+ "CallbackQueryDataRule",
176
+ "CallbackQueryRule",
177
+ "HasData",
178
+ )
@@ -0,0 +1,116 @@
1
+ import dataclasses
2
+ import typing
3
+
4
+ from telegrinder.bot.dispatch.context import Context
5
+
6
+ from .abc import Message
7
+ from .text import TextMessageRule
8
+
9
+ Validator = typing.Callable[[str], typing.Any | None]
10
+
11
+
12
+ def single_split(s: str, separator: str) -> tuple[str, str]:
13
+ left, *right = s.split(separator, 1)
14
+ return left, (right[0] if right else "")
15
+
16
+
17
+ @dataclasses.dataclass(frozen=True)
18
+ class Argument:
19
+ name: str
20
+ validators: list[Validator] = dataclasses.field(default_factory=lambda: [])
21
+ optional: bool = dataclasses.field(default=False, kw_only=True)
22
+
23
+ def check(self, data: str) -> typing.Any | None:
24
+ for validator in self.validators:
25
+ data = validator(data) # type: ignore
26
+ if data is None:
27
+ return None
28
+ return data
29
+
30
+
31
+ class Command(TextMessageRule):
32
+ def __init__(
33
+ self,
34
+ names: str | typing.Iterable[str],
35
+ *arguments: Argument,
36
+ prefixes: tuple[str, ...] = ("/",),
37
+ separator: str = " ",
38
+ lazy: bool = False,
39
+ ) -> None:
40
+ self.names = [names] if isinstance(names, str) else names
41
+ self.arguments = arguments
42
+ self.prefixes = prefixes
43
+ self.separator = separator
44
+ self.lazy = lazy
45
+
46
+ def remove_prefix(self, text: str) -> str | None:
47
+ for prefix in self.prefixes:
48
+ if text.startswith(prefix):
49
+ return text.removeprefix(prefix)
50
+ return None
51
+
52
+ def parse_argument(
53
+ self,
54
+ arguments: list[Argument],
55
+ data_s: str,
56
+ new_s: str,
57
+ s: str,
58
+ ) -> dict | None:
59
+ argument = arguments[0]
60
+ data = argument.check(data_s)
61
+ if data is None and not argument.optional:
62
+ return None
63
+
64
+ if data is None:
65
+ return self.parse_arguments(arguments[1:], s)
66
+
67
+ with_argument = self.parse_arguments(arguments[1:], new_s)
68
+ if with_argument is not None:
69
+ return {argument.name: data, **with_argument}
70
+
71
+ if not argument.optional:
72
+ return None
73
+
74
+ return self.parse_arguments(arguments[1:], s)
75
+
76
+ def parse_arguments(self, arguments: list[Argument], s: str) -> dict | None:
77
+ if not arguments:
78
+ return {} if not s else None
79
+
80
+ if self.lazy:
81
+ return self.parse_argument(arguments, *single_split(s, self.separator), s)
82
+
83
+ all_split = s.split(self.separator)
84
+ for i in range(1, len(all_split) + 1):
85
+ ctx = self.parse_argument(
86
+ arguments,
87
+ self.separator.join(all_split[:i]),
88
+ self.separator.join(all_split[i:]),
89
+ s,
90
+ )
91
+ if ctx is not None:
92
+ return ctx
93
+
94
+ return None
95
+
96
+ async def check(self, message: Message, ctx: Context) -> bool:
97
+ text = self.remove_prefix(message.text.unwrap())
98
+ if text is None:
99
+ return False
100
+
101
+ name, arguments = single_split(text, self.separator)
102
+ if name not in self.names:
103
+ return False
104
+
105
+ if not self.arguments:
106
+ return not arguments
107
+
108
+ result = self.parse_arguments(list(self.arguments), arguments)
109
+ if result is None:
110
+ return False
111
+
112
+ ctx.update(result)
113
+ return True
114
+
115
+
116
+ __all__ = ("Argument", "Command", "single_split")
@@ -0,0 +1,28 @@
1
+ import enum
2
+ import typing
3
+
4
+ from telegrinder.bot.dispatch.context import Context
5
+
6
+ from .abc import Message
7
+ from .text import TextMessageRule
8
+
9
+ T = typing.TypeVar("T", bound=enum.Enum, covariant=True)
10
+
11
+
12
+ class EnumTextRule(TextMessageRule, typing.Generic[T]):
13
+ def __init__(self, enum_t: type[T], *, lower_case: bool = True) -> None:
14
+ self.enum_t = enum_t
15
+ self.texts = list(map(lambda x: x.value.lower() if lower_case else x.value, self.enum_t))
16
+
17
+ def find(self, s: str) -> T:
18
+ for enumeration in self.enum_t:
19
+ if enumeration.value.lower() == s:
20
+ return enumeration
21
+ raise KeyError("Enumeration is undefined.")
22
+
23
+ async def check(self, message: Message, ctx: Context) -> bool:
24
+ text = message.text.unwrap().lower()
25
+ if text not in self.texts:
26
+ return False
27
+ ctx.enum_text = self.find(text)
28
+ return True