telegrinder 0.1.dev170__py3-none-any.whl → 0.2.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.

Files changed (116) hide show
  1. telegrinder/__init__.py +2 -2
  2. telegrinder/api/__init__.py +1 -2
  3. telegrinder/api/api.py +15 -6
  4. telegrinder/api/error.py +2 -1
  5. telegrinder/api/token.py +36 -0
  6. telegrinder/bot/__init__.py +12 -6
  7. telegrinder/bot/bot.py +18 -6
  8. telegrinder/bot/cute_types/__init__.py +7 -7
  9. telegrinder/bot/cute_types/base.py +122 -20
  10. telegrinder/bot/cute_types/callback_query.py +10 -6
  11. telegrinder/bot/cute_types/chat_join_request.py +4 -5
  12. telegrinder/bot/cute_types/chat_member_updated.py +4 -6
  13. telegrinder/bot/cute_types/inline_query.py +3 -4
  14. telegrinder/bot/cute_types/message.py +32 -21
  15. telegrinder/bot/cute_types/update.py +51 -4
  16. telegrinder/bot/cute_types/utils.py +3 -466
  17. telegrinder/bot/dispatch/__init__.py +10 -11
  18. telegrinder/bot/dispatch/abc.py +8 -5
  19. telegrinder/bot/dispatch/context.py +17 -8
  20. telegrinder/bot/dispatch/dispatch.py +71 -48
  21. telegrinder/bot/dispatch/handler/__init__.py +3 -3
  22. telegrinder/bot/dispatch/handler/abc.py +4 -4
  23. telegrinder/bot/dispatch/handler/func.py +46 -22
  24. telegrinder/bot/dispatch/handler/message_reply.py +6 -7
  25. telegrinder/bot/dispatch/middleware/__init__.py +1 -1
  26. telegrinder/bot/dispatch/middleware/abc.py +2 -2
  27. telegrinder/bot/dispatch/process.py +38 -19
  28. telegrinder/bot/dispatch/return_manager/__init__.py +4 -4
  29. telegrinder/bot/dispatch/return_manager/abc.py +3 -3
  30. telegrinder/bot/dispatch/return_manager/callback_query.py +1 -2
  31. telegrinder/bot/dispatch/return_manager/inline_query.py +1 -2
  32. telegrinder/bot/dispatch/return_manager/message.py +1 -2
  33. telegrinder/bot/dispatch/view/__init__.py +8 -8
  34. telegrinder/bot/dispatch/view/abc.py +18 -16
  35. telegrinder/bot/dispatch/view/box.py +75 -64
  36. telegrinder/bot/dispatch/view/callback_query.py +1 -2
  37. telegrinder/bot/dispatch/view/chat_join_request.py +1 -2
  38. telegrinder/bot/dispatch/view/chat_member.py +16 -2
  39. telegrinder/bot/dispatch/view/inline_query.py +1 -2
  40. telegrinder/bot/dispatch/view/message.py +12 -5
  41. telegrinder/bot/dispatch/view/raw.py +9 -8
  42. telegrinder/bot/dispatch/waiter_machine/__init__.py +3 -3
  43. telegrinder/bot/dispatch/waiter_machine/machine.py +12 -8
  44. telegrinder/bot/dispatch/waiter_machine/middleware.py +1 -1
  45. telegrinder/bot/dispatch/waiter_machine/short_state.py +4 -3
  46. telegrinder/bot/polling/abc.py +1 -1
  47. telegrinder/bot/polling/polling.py +6 -6
  48. telegrinder/bot/rules/__init__.py +20 -20
  49. telegrinder/bot/rules/abc.py +57 -43
  50. telegrinder/bot/rules/adapter/__init__.py +5 -5
  51. telegrinder/bot/rules/adapter/abc.py +6 -3
  52. telegrinder/bot/rules/adapter/errors.py +2 -1
  53. telegrinder/bot/rules/adapter/event.py +28 -13
  54. telegrinder/bot/rules/adapter/node.py +28 -22
  55. telegrinder/bot/rules/adapter/raw_update.py +13 -5
  56. telegrinder/bot/rules/callback_data.py +4 -4
  57. telegrinder/bot/rules/chat_join.py +4 -4
  58. telegrinder/bot/rules/command.py +5 -7
  59. telegrinder/bot/rules/func.py +2 -2
  60. telegrinder/bot/rules/fuzzy.py +1 -1
  61. telegrinder/bot/rules/inline.py +3 -3
  62. telegrinder/bot/rules/integer.py +1 -2
  63. telegrinder/bot/rules/markup.py +5 -3
  64. telegrinder/bot/rules/message_entities.py +2 -2
  65. telegrinder/bot/rules/node.py +2 -2
  66. telegrinder/bot/rules/regex.py +1 -1
  67. telegrinder/bot/rules/rule_enum.py +1 -1
  68. telegrinder/bot/rules/text.py +1 -2
  69. telegrinder/bot/rules/update.py +1 -2
  70. telegrinder/bot/scenario/abc.py +2 -2
  71. telegrinder/bot/scenario/checkbox.py +3 -4
  72. telegrinder/bot/scenario/choice.py +1 -2
  73. telegrinder/model.py +89 -45
  74. telegrinder/modules.py +3 -3
  75. telegrinder/msgspec_utils.py +85 -57
  76. telegrinder/node/__init__.py +17 -10
  77. telegrinder/node/attachment.py +19 -16
  78. telegrinder/node/base.py +46 -22
  79. telegrinder/node/callback_query.py +5 -9
  80. telegrinder/node/command.py +6 -2
  81. telegrinder/node/composer.py +102 -77
  82. telegrinder/node/container.py +3 -3
  83. telegrinder/node/event.py +68 -0
  84. telegrinder/node/me.py +3 -0
  85. telegrinder/node/message.py +6 -10
  86. telegrinder/node/polymorphic.py +15 -10
  87. telegrinder/node/rule.py +20 -6
  88. telegrinder/node/scope.py +9 -1
  89. telegrinder/node/source.py +21 -11
  90. telegrinder/node/text.py +4 -4
  91. telegrinder/node/update.py +7 -4
  92. telegrinder/py.typed +0 -0
  93. telegrinder/rules.py +59 -0
  94. telegrinder/tools/__init__.py +2 -2
  95. telegrinder/tools/buttons.py +5 -10
  96. telegrinder/tools/error_handler/abc.py +2 -2
  97. telegrinder/tools/error_handler/error.py +2 -0
  98. telegrinder/tools/error_handler/error_handler.py +6 -6
  99. telegrinder/tools/formatting/spec_html_formats.py +10 -10
  100. telegrinder/tools/global_context/__init__.py +2 -2
  101. telegrinder/tools/global_context/global_context.py +3 -3
  102. telegrinder/tools/global_context/telegrinder_ctx.py +4 -4
  103. telegrinder/tools/keyboard.py +3 -3
  104. telegrinder/tools/loop_wrapper/loop_wrapper.py +47 -13
  105. telegrinder/tools/magic.py +96 -18
  106. telegrinder/types/__init__.py +1 -0
  107. telegrinder/types/enums.py +2 -0
  108. telegrinder/types/methods.py +91 -15
  109. telegrinder/types/objects.py +49 -24
  110. telegrinder/verification_utils.py +1 -3
  111. {telegrinder-0.1.dev170.dist-info → telegrinder-0.2.0.dist-info}/METADATA +2 -2
  112. telegrinder-0.2.0.dist-info/RECORD +145 -0
  113. telegrinder/api/abc.py +0 -73
  114. telegrinder-0.1.dev170.dist-info/RECORD +0 -143
  115. {telegrinder-0.1.dev170.dist-info → telegrinder-0.2.0.dist-info}/LICENSE +0 -0
  116. {telegrinder-0.1.dev170.dist-info → telegrinder-0.2.0.dist-info}/WHEEL +0 -0
@@ -2,16 +2,20 @@ import asyncio
2
2
  import datetime
3
3
  import typing
4
4
 
5
- from telegrinder.api.abc import ABCAPI
5
+ from telegrinder.api import API
6
6
  from telegrinder.bot.dispatch.context import Context
7
7
  from telegrinder.bot.dispatch.view.abc import ABCStateView, BaseStateView
8
+ from telegrinder.bot.dispatch.waiter_machine.middleware import WaiterMiddleware
9
+ from telegrinder.bot.dispatch.waiter_machine.short_state import (
10
+ Behaviour,
11
+ EventModel,
12
+ ShortState,
13
+ ShortStateContext,
14
+ )
8
15
  from telegrinder.bot.rules.abc import ABCRule
9
16
  from telegrinder.tools.limited_dict import LimitedDict
10
17
  from telegrinder.types import Update
11
18
 
12
- from .middleware import WaiterMiddleware
13
- from .short_state import Behaviour, EventModel, ShortState, ShortStateContext
14
-
15
19
  if typing.TYPE_CHECKING:
16
20
  from telegrinder.bot.dispatch import Dispatch
17
21
 
@@ -67,7 +71,7 @@ class WaiterMachine:
67
71
  async def wait(
68
72
  self,
69
73
  state_view: "BaseStateView[EventModel]",
70
- linked: EventModel | tuple[ABCAPI, Identificator],
74
+ linked: EventModel | tuple[API, Identificator],
71
75
  *rules: ABCRule,
72
76
  default: Behaviour[EventModel] | None = None,
73
77
  on_drop: Behaviour[EventModel] | None = None,
@@ -77,7 +81,7 @@ class WaiterMachine:
77
81
  if isinstance(expiration, int | float):
78
82
  expiration = datetime.timedelta(seconds=expiration)
79
83
 
80
- api: ABCAPI
84
+ api: API
81
85
  key: Identificator
82
86
  api, key = linked if isinstance(linked, tuple) else (linked.ctx_api, state_view.get_state_key(linked)) # type: ignore
83
87
  if not key:
@@ -123,7 +127,7 @@ class WaiterMachine:
123
127
 
124
128
  ctx = Context(**context)
125
129
  if await behaviour.check(event.api, update, ctx):
126
- await behaviour.run(event, ctx)
130
+ await behaviour.run(event.api, event, ctx)
127
131
  return True
128
132
 
129
133
  return False
@@ -132,7 +136,7 @@ class WaiterMachine:
132
136
  self,
133
137
  views: typing.Iterable[ABCStateView[EventModel]],
134
138
  absolutely_dead_time: datetime.timedelta = WEEK,
135
- ):
139
+ ) -> None:
136
140
  """Clears storage.
137
141
 
138
142
  :param absolutely_dead_time: timedelta when state can be forgotten.
@@ -83,7 +83,7 @@ class WaiterMiddleware(ABCMiddleware[EventType]):
83
83
  result = await handler.check(event.ctx_api, ctx.raw_update, ctx)
84
84
 
85
85
  if result is True:
86
- await handler.run(event, ctx)
86
+ await handler.run(event.api, event, ctx)
87
87
 
88
88
  elif short_state.default_behaviour is not None:
89
89
  await self.machine.call_behaviour(
@@ -3,7 +3,7 @@ import dataclasses
3
3
  import datetime
4
4
  import typing
5
5
 
6
- from telegrinder.api import ABCAPI
6
+ from telegrinder.api import API
7
7
  from telegrinder.bot.cute_types import BaseCute
8
8
  from telegrinder.bot.dispatch.context import Context
9
9
  from telegrinder.bot.dispatch.handler.abc import ABCHandler
@@ -24,10 +24,10 @@ class ShortStateContext(typing.Generic[EventModel], typing.NamedTuple):
24
24
  context: Context
25
25
 
26
26
 
27
- @dataclasses.dataclass
27
+ @dataclasses.dataclass(slots=True)
28
28
  class ShortState(typing.Generic[EventModel]):
29
29
  key: "Identificator"
30
- ctx_api: ABCAPI
30
+ ctx_api: API
31
31
  event: asyncio.Event
32
32
  rules: tuple[ABCRule, ...]
33
33
  expiration: dataclasses.InitVar[datetime.timedelta | None] = dataclasses.field(
@@ -38,6 +38,7 @@ class ShortState(typing.Generic[EventModel]):
38
38
  on_drop_behaviour: Behaviour[EventModel] | None = dataclasses.field(default=None, kw_only=True)
39
39
  exit_behaviour: Behaviour[EventModel] | None = dataclasses.field(default=None, kw_only=True)
40
40
  expiration_date: datetime.datetime | None = dataclasses.field(init=False, kw_only=True)
41
+ creation_date: datetime.datetime = dataclasses.field(init=False)
41
42
  context: ShortStateContext[EventModel] | None = dataclasses.field(default=None, init=False, kw_only=True)
42
43
 
43
44
  def __post_init__(self, expiration: datetime.timedelta | None = None) -> None:
@@ -3,7 +3,7 @@ from abc import ABC, abstractmethod
3
3
 
4
4
  import msgspec
5
5
 
6
- from telegrinder.types import Update
6
+ from telegrinder.types.objects import Update
7
7
 
8
8
 
9
9
  class ABCPolling(ABC):
@@ -5,25 +5,25 @@ import aiohttp
5
5
  import msgspec
6
6
  from fntypes.result import Error, Ok
7
7
 
8
- from telegrinder.api.abc import ABCAPI
8
+ from telegrinder.api import API
9
9
  from telegrinder.api.error import InvalidTokenError
10
10
  from telegrinder.bot.polling.abc import ABCPolling
11
11
  from telegrinder.modules import logger
12
12
  from telegrinder.msgspec_utils import decoder
13
- from telegrinder.types import Update, UpdateType
13
+ from telegrinder.types.objects import Update, UpdateType
14
14
 
15
15
 
16
16
  class Polling(ABCPolling):
17
17
  def __init__(
18
18
  self,
19
- api: ABCAPI,
19
+ api: API,
20
20
  *,
21
21
  offset: int = 0,
22
22
  reconnection_timeout: float = 5,
23
23
  max_reconnetions: int = 10,
24
24
  include_updates: set[str | UpdateType] | None = None,
25
25
  exclude_updates: set[str | UpdateType] | None = None,
26
- ):
26
+ ) -> None:
27
27
  self.api = api
28
28
  self.allowed_updates = self.get_allowed_updates(
29
29
  include_updates=include_updates,
@@ -48,8 +48,8 @@ class Polling(ABCPolling):
48
48
  self.reconnection_timeout,
49
49
  )
50
50
 
51
+ @staticmethod
51
52
  def get_allowed_updates(
52
- self,
53
53
  *,
54
54
  include_updates: set[str | UpdateType] | None = None,
55
55
  exclude_updates: set[str | UpdateType] | None = None,
@@ -111,7 +111,7 @@ class Polling(ABCPolling):
111
111
  exit(6)
112
112
  else:
113
113
  logger.warning(
114
- "Server disconnected, waiting 5 seconds to reconnet...",
114
+ "Server disconnected, waiting 5 seconds to reconnect...",
115
115
  )
116
116
  reconn_counter += 1
117
117
  await asyncio.sleep(self.reconnection_timeout)
@@ -1,5 +1,5 @@
1
- from .abc import ABCRule, AndRule, NotRule, OrRule
2
- from .callback_data import (
1
+ from telegrinder.bot.rules.abc import ABCRule, AndRule, NotRule, OrRule
2
+ from telegrinder.bot.rules.callback_data import (
3
3
  CallbackDataEq,
4
4
  CallbackDataJsonEq,
5
5
  CallbackDataJsonModel,
@@ -9,25 +9,25 @@ from .callback_data import (
9
9
  CallbackQueryRule,
10
10
  HasData,
11
11
  )
12
- from .chat_join import (
12
+ from telegrinder.bot.rules.chat_join import (
13
13
  ChatJoinRequestRule,
14
14
  HasInviteLink,
15
15
  InviteLinkByCreator,
16
16
  InviteLinkName,
17
17
  )
18
- from .command import Argument, Command
19
- from .enum_text import EnumTextRule
20
- from .func import FuncRule
21
- from .fuzzy import FuzzyText
22
- from .inline import (
18
+ from telegrinder.bot.rules.command import Argument, Command
19
+ from telegrinder.bot.rules.enum_text import EnumTextRule
20
+ from telegrinder.bot.rules.func import FuncRule
21
+ from telegrinder.bot.rules.fuzzy import FuzzyText
22
+ from telegrinder.bot.rules.inline import (
23
23
  HasLocation,
24
24
  InlineQueryChatType,
25
25
  InlineQueryMarkup,
26
26
  InlineQueryRule,
27
27
  InlineQueryText,
28
28
  )
29
- from .integer import IntegerInRange, IsInteger
30
- from .is_from import (
29
+ from telegrinder.bot.rules.integer import IntegerInRange, IsInteger
30
+ from telegrinder.bot.rules.is_from import (
31
31
  IsBot,
32
32
  IsChat,
33
33
  IsChatId,
@@ -45,16 +45,16 @@ from .is_from import (
45
45
  IsUser,
46
46
  IsUserId,
47
47
  )
48
- from .markup import Markup
49
- from .mention import HasMention
50
- from .message import MessageRule
51
- from .message_entities import HasEntities, MessageEntities
52
- from .node import NodeRule
53
- from .regex import Regex
54
- from .rule_enum import RuleEnum
55
- from .start import StartCommand
56
- from .text import HasText, Text
57
- from .update import IsUpdateType
48
+ from telegrinder.bot.rules.markup import Markup
49
+ from telegrinder.bot.rules.mention import HasMention
50
+ from telegrinder.bot.rules.message import MessageRule
51
+ from telegrinder.bot.rules.message_entities import HasEntities, MessageEntities
52
+ from telegrinder.bot.rules.node import NodeRule
53
+ from telegrinder.bot.rules.regex import Regex
54
+ from telegrinder.bot.rules.rule_enum import RuleEnum
55
+ from telegrinder.bot.rules.start import StartCommand
56
+ from telegrinder.bot.rules.text import HasText, Text
57
+ from telegrinder.bot.rules.update import IsUpdateType
58
58
 
59
59
  __all__ = (
60
60
  "ABCRule",
@@ -1,5 +1,6 @@
1
1
  import inspect
2
2
  from abc import ABC, abstractmethod
3
+ from functools import cached_property
3
4
 
4
5
  import typing_extensions as typing
5
6
 
@@ -8,12 +9,19 @@ from telegrinder.bot.dispatch.context import Context
8
9
  from telegrinder.bot.dispatch.process import check_rule
9
10
  from telegrinder.bot.rules.adapter import ABCAdapter, RawUpdateAdapter
10
11
  from telegrinder.bot.rules.adapter.node import Event
11
- from telegrinder.node.base import Node, is_node
12
- from telegrinder.node.composer import NodeCollection
12
+ from telegrinder.node.base import Node, get_nodes, is_node
13
13
  from telegrinder.tools.i18n.base import ABCTranslator
14
- from telegrinder.tools.magic import cache_translation, get_annotations, get_cached_translation
14
+ from telegrinder.tools.magic import (
15
+ cache_translation,
16
+ get_annotations,
17
+ get_cached_translation,
18
+ get_default_args,
19
+ )
15
20
  from telegrinder.types.objects import Update as UpdateObject
16
21
 
22
+ if typing.TYPE_CHECKING:
23
+ from telegrinder.node.composer import NodeCollection
24
+
17
25
  AdaptTo = typing.TypeVar("AdaptTo", default=typing.Any)
18
26
 
19
27
  Message: typing.TypeAlias = MessageCute
@@ -34,50 +42,16 @@ def with_caching_translations(func: typing.Callable[..., typing.Any]):
34
42
 
35
43
 
36
44
  class ABCRule(ABC, typing.Generic[AdaptTo]):
37
- adapter: ABCAdapter[UpdateObject, AdaptTo] = RawUpdateAdapter() # type: ignore
45
+ adapter: ABCAdapter[UpdateObject, AdaptTo]
38
46
  requires: list["ABCRule"] = []
39
47
 
48
+ if not typing.TYPE_CHECKING:
49
+ adapter = RawUpdateAdapter()
50
+
40
51
  @abstractmethod
41
52
  async def check(self, event: AdaptTo, *, ctx: Context) -> bool:
42
53
  pass
43
54
 
44
- def get_required_nodes(self) -> dict[str, type[Node]]:
45
- return {k: v for k, v in get_annotations(self.check).items() if is_node(v)}
46
-
47
- async def bounding_check(
48
- self,
49
- adapted_value: AdaptTo,
50
- ctx: Context,
51
- node_col: NodeCollection | None = None,
52
- ) -> bool:
53
- kw = {}
54
- node_col_values = node_col.values() if node_col is not None else {}
55
-
56
- for i, (k, v) in enumerate(get_annotations(self.check).items()):
57
- if (isinstance(adapted_value, Event) and not i) or (
58
- isinstance(v, type) and isinstance(adapted_value, v)
59
- ):
60
- kw[k] = adapted_value if not isinstance(adapted_value, Event) else adapted_value.obj
61
- elif is_node(v):
62
- assert k in node_col_values, "Node is undefined, error while bounding."
63
- kw[k] = node_col_values[k]
64
- elif k in ctx:
65
- kw[k] = ctx[k]
66
- elif v is Context:
67
- kw[k] = ctx
68
- else:
69
- raise LookupError(
70
- f"Cannot bound {k!r} of type {v!r} to '{self.__class__.__name__}.check()', because it cannot be resolved."
71
- )
72
-
73
- return await self.check(**kw)
74
-
75
- def optional(self) -> "ABCRule":
76
- return self | Always()
77
-
78
- def should_fail(self) -> "ABCRule":
79
- return self & Never()
80
-
81
55
  def __init_subclass__(cls, requires: list["ABCRule"] | None = None) -> None:
82
56
  """Merges requirements from inherited classes and rule-specific requirements."""
83
57
 
@@ -128,6 +102,46 @@ class ABCRule(ABC, typing.Generic[AdaptTo]):
128
102
  self.adapter,
129
103
  )
130
104
 
105
+ @cached_property
106
+ def required_nodes(self) -> dict[str, type[Node]]:
107
+ return get_nodes(self.check)
108
+
109
+ def as_optional(self) -> "ABCRule":
110
+ return self | Always()
111
+
112
+ def should_fail(self) -> "ABCRule":
113
+ return self & Never()
114
+
115
+ async def bounding_check(
116
+ self,
117
+ adapted_value: AdaptTo,
118
+ ctx: Context,
119
+ node_col: "NodeCollection | None" = None,
120
+ ) -> bool:
121
+ kw = {}
122
+ node_col_values = node_col.values if node_col is not None else {}
123
+ temp_ctx = get_default_args(self.check) | ctx
124
+
125
+ for i, (k, v) in enumerate(get_annotations(self.check).items()):
126
+ if (isinstance(adapted_value, Event) and not i) or (
127
+ isinstance(v, type) and isinstance(adapted_value, v)
128
+ ):
129
+ kw[k] = adapted_value if not isinstance(adapted_value, Event) else adapted_value.obj
130
+ elif is_node(v):
131
+ assert k in node_col_values, "Node is undefined, error while bounding."
132
+ kw[k] = node_col_values[k]
133
+ elif k in temp_ctx:
134
+ kw[k] = temp_ctx[k]
135
+ elif v is Context:
136
+ kw[k] = ctx
137
+ else:
138
+ raise LookupError(
139
+ f"Cannot bound {k!r} of type {v!r} to '{self.__class__.__qualname__}.check()', "
140
+ "because it cannot be resolved."
141
+ )
142
+
143
+ return await self.check(**kw)
144
+
131
145
  async def translate(self, translator: ABCTranslator) -> typing.Self:
132
146
  return self
133
147
 
@@ -179,10 +193,10 @@ class Always(ABCRule):
179
193
 
180
194
  __all__ = (
181
195
  "ABCRule",
196
+ "Always",
182
197
  "AndRule",
198
+ "Never",
183
199
  "NotRule",
184
200
  "OrRule",
185
201
  "with_caching_translations",
186
- "Never",
187
- "Always",
188
202
  )
@@ -1,8 +1,8 @@
1
- from .abc import ABCAdapter, Event
2
- from .errors import AdapterError
3
- from .event import EventAdapter
4
- from .node import NodeAdapter
5
- from .raw_update import RawUpdateAdapter
1
+ from telegrinder.bot.rules.adapter.abc import ABCAdapter, Event
2
+ from telegrinder.bot.rules.adapter.errors import AdapterError
3
+ from telegrinder.bot.rules.adapter.event import EventAdapter
4
+ from telegrinder.bot.rules.adapter.node import NodeAdapter
5
+ from telegrinder.bot.rules.adapter.raw_update import RawUpdateAdapter
6
6
 
7
7
  __all__ = (
8
8
  "ABCAdapter",
@@ -4,7 +4,8 @@ import typing
4
4
 
5
5
  from fntypes.result import Result
6
6
 
7
- from telegrinder.api.abc import ABCAPI
7
+ from telegrinder.api import API
8
+ from telegrinder.bot.dispatch.context import Context
8
9
  from telegrinder.bot.rules.adapter.errors import AdapterError
9
10
  from telegrinder.model import Model
10
11
 
@@ -13,12 +14,14 @@ To = typing.TypeVar("To")
13
14
 
14
15
 
15
16
  class ABCAdapter(abc.ABC, typing.Generic[From, To]):
17
+ ADAPTED_VALUE_KEY: str | None = None
18
+
16
19
  @abc.abstractmethod
17
- async def adapt(self, api: ABCAPI, update: From) -> Result[To, AdapterError]:
20
+ async def adapt(self, api: API, update: From, context: Context) -> Result[To, AdapterError]:
18
21
  pass
19
22
 
20
23
 
21
- @dataclasses.dataclass
24
+ @dataclasses.dataclass(slots=True)
22
25
  class Event(typing.Generic[To]):
23
26
  obj: To
24
27
 
@@ -1,4 +1,5 @@
1
- class AdapterError(RuntimeError): ...
1
+ class AdapterError(RuntimeError):
2
+ pass
2
3
 
3
4
 
4
5
  __all__ = ("AdapterError",)
@@ -2,8 +2,9 @@ import typing
2
2
 
3
3
  from fntypes.result import Error, Ok, Result
4
4
 
5
- from telegrinder.api.abc import ABCAPI
6
- from telegrinder.bot.cute_types import BaseCute
5
+ from telegrinder.api import API
6
+ from telegrinder.bot.cute_types.base import BaseCute
7
+ from telegrinder.bot.dispatch.context import Context
7
8
  from telegrinder.bot.rules.adapter.abc import ABCAdapter
8
9
  from telegrinder.bot.rules.adapter.errors import AdapterError
9
10
  from telegrinder.msgspec_utils import Nothing
@@ -14,6 +15,8 @@ ToCute = typing.TypeVar("ToCute", bound=BaseCute)
14
15
 
15
16
 
16
17
  class EventAdapter(ABCAdapter[Update, ToCute]):
18
+ ADAPTED_VALUE_KEY: str = "_adapted_cute_event"
19
+
17
20
  def __init__(self, event: UpdateType | type[Model], cute_model: type[ToCute]) -> None:
18
21
  self.event = event
19
22
  self.cute_model = cute_model
@@ -29,33 +32,45 @@ class EventAdapter(ABCAdapter[Update, ToCute]):
29
32
  else:
30
33
  raw_update_type = self.event.__name__
31
34
 
32
- return "<{}: adapt {} -> {}>".format(
35
+ return "<{}: adapt Update -> {} -> {}>".format(
33
36
  self.__class__.__name__,
34
37
  raw_update_type,
35
38
  self.cute_model.__name__,
36
39
  )
37
40
 
38
- async def adapt(self, api: ABCAPI, update: Update) -> Result[ToCute, AdapterError]:
39
- update_dct = update.to_dict()
41
+ async def adapt(self, api: API, update: Update, context: Context) -> Result[ToCute, AdapterError]:
42
+ if self.ADAPTED_VALUE_KEY in context:
43
+ return Ok(context[self.ADAPTED_VALUE_KEY])
44
+
40
45
  if isinstance(self.event, UpdateType):
41
46
  if update.update_type != self.event:
42
47
  return Error(
43
48
  AdapterError(f"Update is not of event type {self.event!r}."),
44
49
  )
45
50
 
46
- if update_dct[self.event.value] is Nothing:
51
+ if isinstance(event := getattr(update, self.event.value, Nothing), type(Nothing)):
47
52
  return Error(
48
53
  AdapterError(f"Update is not an {self.event!r}."),
49
54
  )
50
55
 
51
- return Ok(
52
- self.cute_model.from_update(update_dct[self.event.value].unwrap(), bound_api=api),
53
- )
56
+ event = event.unwrap()
57
+
58
+ if type(event) is self.cute_model:
59
+ adapted = event
60
+ else:
61
+ adapted = self.cute_model.from_update(event, bound_api=api)
62
+ else:
63
+ event = getattr(update, update.update_type.value).unwrap()
64
+ if not update.update_type or not issubclass(type(event), self.event):
65
+ return Error(AdapterError(f"Update is not an {self.event.__name__!r}."))
66
+
67
+ if type(event) is self.cute_model:
68
+ adapted = event
69
+ else:
70
+ adapted = self.cute_model.from_update(event, bound_api=api)
54
71
 
55
- event = update_dct[update.update_type.value].unwrap()
56
- if not update.update_type or not issubclass(event.__class__, self.event):
57
- return Error(AdapterError(f"Update is not an {self.event.__name__!r}."))
58
- return Ok(self.cute_model.from_update(event, bound_api=api))
72
+ context[self.ADAPTED_VALUE_KEY] = adapted
73
+ return Ok(adapted) # type: ignore
59
74
 
60
75
 
61
76
  __all__ = ("EventAdapter",)
@@ -1,17 +1,18 @@
1
- import typing
2
-
1
+ import typing_extensions as typing
3
2
  from fntypes.result import Error, Ok, Result
4
3
 
5
- from telegrinder.api.abc import ABCAPI
6
- from telegrinder.bot.cute_types.update import UpdateCute
4
+ from telegrinder.api import API
7
5
  from telegrinder.bot.dispatch.context import Context
8
6
  from telegrinder.bot.rules.adapter.abc import ABCAdapter, Event
9
7
  from telegrinder.bot.rules.adapter.errors import AdapterError
10
- from telegrinder.node.base import ComposeError
11
- from telegrinder.node.composer import NodeSession, compose_node
8
+ from telegrinder.msgspec_utils import repr_type
9
+ from telegrinder.node.composer import NodeSession, compose_nodes
12
10
  from telegrinder.types.objects import Update
13
11
 
14
- Ts = typing.TypeVarTuple("Ts")
12
+ if typing.TYPE_CHECKING:
13
+ from telegrinder.node.base import Node
14
+
15
+ Ts = typing.TypeVarTuple("Ts", default=typing.Unpack[tuple[type["Node"], ...]])
15
16
 
16
17
 
17
18
  class NodeAdapter(typing.Generic[*Ts], ABCAdapter[Update, Event[tuple[*Ts]]]):
@@ -19,24 +20,29 @@ class NodeAdapter(typing.Generic[*Ts], ABCAdapter[Update, Event[tuple[*Ts]]]):
19
20
  self.nodes = nodes
20
21
 
21
22
  def __repr__(self) -> str:
22
- return "<{}: adapt Update -> {}>".format(
23
+ return "<{}: adapt Update -> ({})>".format(
23
24
  self.__class__.__name__,
24
- Update.__name__,
25
- ", ".join(node.__name__ for node in self.nodes), # type: ignore
25
+ ", ".join(repr_type(node) for node in self.nodes),
26
+ )
27
+
28
+ async def adapt(
29
+ self,
30
+ api: API,
31
+ update: Update,
32
+ context: Context,
33
+ ) -> Result[Event[tuple[*Ts]], AdapterError]:
34
+ result = await compose_nodes(
35
+ nodes={str(i): typing.cast(type["Node"], node) for i, node in enumerate(self.nodes)},
36
+ ctx=context,
37
+ data={Update: update, API: api},
26
38
  )
27
39
 
28
- async def adapt(self, api: ABCAPI, update: Update) -> Result[Event[tuple[*Ts]], AdapterError]:
29
- update_cute = UpdateCute.from_update(update, api)
30
- node_sessions: list[NodeSession] = []
31
- for node_t in self.nodes:
32
- try:
33
- # FIXME: adapters should have context
34
- node_sessions.append(await compose_node(node_t, update_cute, Context())) # type: ignore
35
- except ComposeError:
36
- for session in node_sessions:
37
- await session.close(with_value=None)
38
- return Error(AdapterError(f"Couldn't compose nodes, error on {node_t}"))
39
- return Ok(Event(tuple(node_sessions))) # type: ignore
40
+ match result:
41
+ case Ok(collection):
42
+ sessions: list[NodeSession] = list(collection.sessions.values())
43
+ return Ok(Event(tuple(sessions))) # type: ignore
44
+ case Error(err):
45
+ return Error(AdapterError(f"Couldn't compose nodes, error: {err}."))
40
46
 
41
47
 
42
48
  __all__ = ("NodeAdapter",)
@@ -1,24 +1,32 @@
1
1
  from fntypes.result import Ok, Result
2
2
 
3
- from telegrinder.api.abc import ABCAPI
3
+ from telegrinder.api import API
4
4
  from telegrinder.bot.cute_types.update import UpdateCute
5
+ from telegrinder.bot.dispatch.context import Context
5
6
  from telegrinder.bot.rules.adapter.abc import ABCAdapter
6
7
  from telegrinder.bot.rules.adapter.errors import AdapterError
7
8
  from telegrinder.types.objects import Update
8
9
 
9
10
 
10
11
  class RawUpdateAdapter(ABCAdapter[Update, UpdateCute]):
12
+ ADAPTED_VALUE_KEY: str = "_adapted_update_cute"
13
+
11
14
  def __repr__(self) -> str:
12
15
  return f"<{self.__class__.__name__}: adapt Update -> UpdateCute>"
13
16
 
14
17
  async def adapt(
15
18
  self,
16
- api: ABCAPI,
19
+ api: API,
17
20
  update: Update,
21
+ context: Context,
18
22
  ) -> Result[UpdateCute, AdapterError]:
19
- if not isinstance(update, UpdateCute):
20
- return Ok(UpdateCute.from_update(update, api))
21
- return Ok(update)
23
+ if self.ADAPTED_VALUE_KEY not in context:
24
+ context[self.ADAPTED_VALUE_KEY] = (
25
+ UpdateCute.from_update(update, api)
26
+ if not isinstance(update, UpdateCute)
27
+ else update
28
+ )
29
+ return Ok(context[self.ADAPTED_VALUE_KEY])
22
30
 
23
31
 
24
32
  __all__ = ("RawUpdateAdapter",)
@@ -26,12 +26,12 @@ class CallbackQueryRule(ABCRule[CallbackQuery], abc.ABC):
26
26
  adapter: EventAdapter[CallbackQuery] = EventAdapter(UpdateType.CALLBACK_QUERY, CallbackQuery)
27
27
 
28
28
  @abc.abstractmethod
29
- async def check(self, event: CallbackQuery, ctx: Context) -> bool:
29
+ async def check(self, event: CallbackQuery, context: Context) -> bool:
30
30
  pass
31
31
 
32
32
 
33
33
  class HasData(CallbackQueryRule):
34
- async def check(self, event: CallbackQuery, ctx: Context) -> bool:
34
+ async def check(self, event: CallbackQuery) -> bool:
35
35
  return bool(event.data.unwrap_or_none())
36
36
 
37
37
 
@@ -122,7 +122,7 @@ class CallbackDataEq(CallbackQueryDataRule):
122
122
  def __init__(self, value: str, /) -> None:
123
123
  self.value = value
124
124
 
125
- async def check(self, event: CallbackQuery, ctx: Context) -> bool:
125
+ async def check(self, event: CallbackQuery) -> bool:
126
126
  return event.data.unwrap() == self.value
127
127
 
128
128
 
@@ -130,7 +130,7 @@ class CallbackDataJsonEq(CallbackQueryDataRule):
130
130
  def __init__(self, d: dict[str, typing.Any], /) -> None:
131
131
  self.d = d
132
132
 
133
- async def check(self, event: CallbackQuery, ctx: Context) -> bool:
133
+ async def check(self, event: CallbackQuery) -> bool:
134
134
  return event.decode_callback_data().unwrap_or_none() == self.d
135
135
 
136
136
 
@@ -15,12 +15,12 @@ class ChatJoinRequestRule(ABCRule[ChatJoinRequest], requires=[]):
15
15
  adapter: EventAdapter[ChatJoinRequest] = EventAdapter(UpdateType.CHAT_JOIN_REQUEST, ChatJoinRequest)
16
16
 
17
17
  @abc.abstractmethod
18
- async def check(self, event: ChatJoinRequest, ctx: Context) -> bool:
18
+ async def check(self, event: ChatJoinRequest, context: Context) -> bool:
19
19
  pass
20
20
 
21
21
 
22
22
  class HasInviteLink(ChatJoinRequestRule):
23
- async def check(self, event: ChatJoinRequest, ctx: Context) -> bool:
23
+ async def check(self, event: ChatJoinRequest) -> bool:
24
24
  return bool(event.invite_link)
25
25
 
26
26
 
@@ -28,7 +28,7 @@ class InviteLinkName(ChatJoinRequestRule, requires=[HasInviteLink()]):
28
28
  def __init__(self, name: str, /) -> None:
29
29
  self.name = name
30
30
 
31
- async def check(self, event: ChatJoinRequest, ctx: Context) -> bool:
31
+ async def check(self, event: ChatJoinRequest) -> bool:
32
32
  return event.invite_link.unwrap().name.unwrap_or_none() == self.name
33
33
 
34
34
 
@@ -36,7 +36,7 @@ class InviteLinkByCreator(ChatJoinRequestRule, requires=[HasInviteLink()]):
36
36
  def __init__(self, creator_id: int, /) -> None:
37
37
  self.creator_id = creator_id
38
38
 
39
- async def check(self, event: ChatJoinRequest, ctx: Context) -> bool:
39
+ async def check(self, event: ChatJoinRequest) -> bool:
40
40
  return event.invite_link.unwrap().creator.id == self.creator_id
41
41
 
42
42