telegrinder 0.1.dev166__py3-none-any.whl → 0.1.dev168__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 (64) hide show
  1. telegrinder/__init__.py +0 -2
  2. telegrinder/bot/__init__.py +0 -2
  3. telegrinder/bot/bot.py +1 -3
  4. telegrinder/bot/cute_types/base.py +3 -12
  5. telegrinder/bot/cute_types/callback_query.py +2 -4
  6. telegrinder/bot/cute_types/chat_join_request.py +3 -1
  7. telegrinder/bot/cute_types/chat_member_updated.py +3 -1
  8. telegrinder/bot/cute_types/message.py +10 -31
  9. telegrinder/bot/cute_types/utils.py +1 -3
  10. telegrinder/bot/dispatch/__init__.py +1 -2
  11. telegrinder/bot/dispatch/composition.py +1 -3
  12. telegrinder/bot/dispatch/dispatch.py +5 -6
  13. telegrinder/bot/dispatch/handler/func.py +4 -11
  14. telegrinder/bot/dispatch/return_manager/abc.py +9 -13
  15. telegrinder/bot/dispatch/return_manager/message.py +5 -7
  16. telegrinder/bot/dispatch/view/abc.py +54 -5
  17. telegrinder/bot/dispatch/view/box.py +3 -11
  18. telegrinder/bot/dispatch/view/raw.py +2 -6
  19. telegrinder/bot/dispatch/waiter_machine/__init__.py +1 -2
  20. telegrinder/bot/dispatch/waiter_machine/machine.py +43 -88
  21. telegrinder/bot/dispatch/waiter_machine/middleware.py +12 -5
  22. telegrinder/bot/dispatch/waiter_machine/short_state.py +15 -5
  23. telegrinder/bot/polling/polling.py +2 -6
  24. telegrinder/bot/rules/adapter/event.py +1 -3
  25. telegrinder/bot/rules/callback_data.py +8 -8
  26. telegrinder/bot/rules/fuzzy.py +1 -2
  27. telegrinder/bot/rules/is_from.py +6 -4
  28. telegrinder/bot/rules/markup.py +1 -2
  29. telegrinder/bot/rules/mention.py +1 -4
  30. telegrinder/bot/rules/regex.py +1 -2
  31. telegrinder/bot/rules/rule_enum.py +1 -3
  32. telegrinder/bot/rules/start.py +1 -3
  33. telegrinder/bot/scenario/checkbox.py +6 -10
  34. telegrinder/bot/scenario/choice.py +4 -3
  35. telegrinder/client/aiohttp.py +1 -3
  36. telegrinder/model.py +4 -3
  37. telegrinder/modules.py +1 -3
  38. telegrinder/msgspec_utils.py +1 -3
  39. telegrinder/node/attachment.py +18 -14
  40. telegrinder/node/base.py +4 -11
  41. telegrinder/node/composer.py +1 -3
  42. telegrinder/node/message.py +3 -1
  43. telegrinder/node/source.py +3 -1
  44. telegrinder/node/text.py +3 -1
  45. telegrinder/tools/__init__.py +2 -0
  46. telegrinder/tools/buttons.py +4 -6
  47. telegrinder/tools/error_handler/abc.py +1 -3
  48. telegrinder/tools/error_handler/error.py +3 -6
  49. telegrinder/tools/error_handler/error_handler.py +17 -13
  50. telegrinder/tools/formatting/html.py +2 -6
  51. telegrinder/tools/formatting/links.py +1 -3
  52. telegrinder/tools/global_context/abc.py +1 -3
  53. telegrinder/tools/global_context/global_context.py +13 -31
  54. telegrinder/tools/i18n/middleware/base.py +1 -3
  55. telegrinder/tools/limited_dict.py +37 -0
  56. telegrinder/tools/loop_wrapper/loop_wrapper.py +3 -7
  57. telegrinder/types/__init__.py +30 -0
  58. telegrinder/types/methods.py +20 -89
  59. telegrinder/types/objects.py +16 -45
  60. telegrinder/verification_utils.py +2 -1
  61. {telegrinder-0.1.dev166.dist-info → telegrinder-0.1.dev168.dist-info}/METADATA +5 -5
  62. {telegrinder-0.1.dev166.dist-info → telegrinder-0.1.dev168.dist-info}/RECORD +64 -63
  63. {telegrinder-0.1.dev166.dist-info → telegrinder-0.1.dev168.dist-info}/LICENSE +0 -0
  64. {telegrinder-0.1.dev166.dist-info → telegrinder-0.1.dev168.dist-info}/WHEEL +0 -0
@@ -14,21 +14,15 @@ from .message import MessageNode
14
14
  class Attachment(DataNode):
15
15
  attachment_type: typing.Literal["audio", "document", "photo", "poll", "video"]
16
16
  _: dataclasses.KW_ONLY
17
- audio: Option[telegrinder.types.Audio] = dataclasses.field(
18
- default_factory=lambda: Nothing()
19
- )
17
+ audio: Option[telegrinder.types.Audio] = dataclasses.field(default_factory=lambda: Nothing())
20
18
  document: Option[telegrinder.types.Document] = dataclasses.field(
21
19
  default_factory=lambda: Nothing()
22
20
  )
23
21
  photo: Option[list[telegrinder.types.PhotoSize]] = dataclasses.field(
24
22
  default_factory=lambda: Nothing()
25
23
  )
26
- poll: Option[telegrinder.types.Poll] = dataclasses.field(
27
- default_factory=lambda: Nothing()
28
- )
29
- video: Option[telegrinder.types.Video] = dataclasses.field(
30
- default_factory=lambda: Nothing()
31
- )
24
+ poll: Option[telegrinder.types.Poll] = dataclasses.field(default_factory=lambda: Nothing())
25
+ video: Option[telegrinder.types.Video] = dataclasses.field(default_factory=lambda: Nothing())
32
26
 
33
27
  @classmethod
34
28
  async def compose(cls, message: MessageNode) -> "Attachment":
@@ -44,31 +38,41 @@ class Photo(DataNode):
44
38
 
45
39
  @classmethod
46
40
  async def compose(cls, attachment: Attachment) -> typing.Self:
47
- return cls(attachment.photo.expect(ComposeError("Attachment is not an photo")))
41
+ if not attachment.photo:
42
+ raise ComposeError("Attachment is not an photo")
43
+ return cls(attachment.photo.unwrap())
48
44
 
49
45
 
50
46
  class Video(ScalarNode, telegrinder.types.Video):
51
47
  @classmethod
52
48
  async def compose(cls, attachment: Attachment) -> telegrinder.types.Video:
53
- return attachment.video.expect(ComposeError("Attachment is not an video"))
49
+ if not attachment.video:
50
+ raise ComposeError("Attachment is not an video")
51
+ return attachment.video.unwrap()
54
52
 
55
53
 
56
54
  class Audio(ScalarNode, telegrinder.types.Audio):
57
55
  @classmethod
58
56
  async def compose(cls, attachment: Attachment) -> telegrinder.types.Audio:
59
- return attachment.audio.expect(ComposeError("Attachment is not an audio"))
57
+ if not attachment.audio:
58
+ raise ComposeError("Attachment is not an audio")
59
+ return attachment.audio.unwrap()
60
60
 
61
61
 
62
62
  class Document(ScalarNode, telegrinder.types.Document):
63
63
  @classmethod
64
64
  async def compose(cls, attachment: Attachment) -> telegrinder.types.Document:
65
- return attachment.document.expect(ComposeError("Attachment is not an document"))
65
+ if not attachment.document:
66
+ raise ComposeError("Attachment is not an document")
67
+ return attachment.document.unwrap()
66
68
 
67
69
 
68
70
  class Poll(ScalarNode, telegrinder.types.Poll):
69
71
  @classmethod
70
72
  async def compose(cls, attachment: Attachment) -> telegrinder.types.Poll:
71
- return attachment.poll.expect(ComposeError("Attachment is not an poll"))
73
+ if not attachment.poll:
74
+ raise ComposeError("Attachment is not an poll")
75
+ return attachment.poll.unwrap()
72
76
 
73
77
 
74
78
  __all__ = (
telegrinder/node/base.py CHANGED
@@ -3,8 +3,7 @@ import inspect
3
3
  import typing
4
4
 
5
5
  ComposeResult: typing.TypeAlias = (
6
- typing.Coroutine[typing.Any, typing.Any, typing.Any]
7
- | typing.AsyncGenerator[typing.Any, None]
6
+ typing.Coroutine[typing.Any, typing.Any, typing.Any] | typing.AsyncGenerator[typing.Any, None]
8
7
  )
9
8
 
10
9
 
@@ -16,9 +15,7 @@ class Node(abc.ABC):
16
15
 
17
16
  @classmethod
18
17
  @abc.abstractmethod
19
- def compose(
20
- cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any
21
- ) -> ComposeResult:
18
+ def compose(cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any) -> ComposeResult:
22
19
  pass
23
20
 
24
21
  @classmethod
@@ -52,18 +49,14 @@ class DataNode(Node, abc.ABC):
52
49
  @typing.dataclass_transform()
53
50
  @classmethod
54
51
  @abc.abstractmethod
55
- async def compose(
56
- cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any
57
- ) -> ComposeResult:
52
+ async def compose(cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any) -> ComposeResult:
58
53
  pass
59
54
 
60
55
 
61
56
  class ScalarNodeProto(Node, abc.ABC):
62
57
  @classmethod
63
58
  @abc.abstractmethod
64
- async def compose(
65
- cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any
66
- ) -> ComposeResult:
59
+ async def compose(cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any) -> ComposeResult:
67
60
  pass
68
61
 
69
62
 
@@ -27,9 +27,7 @@ class NodeSession:
27
27
  self.generator = None
28
28
 
29
29
  def __repr__(self) -> str:
30
- return f"<{self.__class__.__name__}: {self.value}" + (
31
- "ACTIVE>" if self.generator else ">"
32
- )
30
+ return f"<{self.__class__.__name__}: {self.value}" + ("ACTIVE>" if self.generator else ">")
33
31
 
34
32
 
35
33
  class NodeCollection:
@@ -9,8 +9,10 @@ from .update import UpdateNode
9
9
  class MessageNode(ScalarNode, MessageCute):
10
10
  @classmethod
11
11
  async def compose(cls, update: UpdateNode) -> typing.Self:
12
+ if not update.message:
13
+ raise ComposeError
12
14
  return cls(
13
- **update.message.expect(ComposeError).to_dict(),
15
+ **update.message.unwrap().to_dict(),
14
16
  api=update.api,
15
17
  )
16
18
 
@@ -24,7 +24,9 @@ class Source(DataNode):
24
24
 
25
25
  async def send(self, text: str) -> Message:
26
26
  result = await self.api.send_message(
27
- self.chat.id, message_thread_id=self.thread_id, text=text
27
+ chat_id=self.chat.id,
28
+ message_thread_id=self.thread_id,
29
+ text=text,
28
30
  )
29
31
  return result.unwrap()
30
32
 
telegrinder/node/text.py CHANGED
@@ -7,7 +7,9 @@ from .message import MessageNode
7
7
  class Text(ScalarNode, str):
8
8
  @classmethod
9
9
  async def compose(cls, message: MessageNode) -> typing.Self:
10
- return cls(message.text.expect(ComposeError("Message has no text")))
10
+ if not message.text:
11
+ raise ComposeError("Message has no text")
12
+ return cls(message.text.unwrap())
11
13
 
12
14
 
13
15
  __all__ = ("Text",)
@@ -66,6 +66,7 @@ from .keyboard import (
66
66
  Keyboard,
67
67
  RowButtons,
68
68
  )
69
+ from .limited_dict import LimitedDict
69
70
  from .loop_wrapper import ABCLoopWrapper, DelayedTask, Lifespan, LoopWrapper
70
71
  from .magic import magic_bundle, resolve_arg_names
71
72
  from .parse_mode import ParseMode
@@ -99,6 +100,7 @@ __all__ = (
99
100
  "KeyboardSetBase",
100
101
  "KeyboardSetYAML",
101
102
  "Lifespan",
103
+ "LimitedDict",
102
104
  "Link",
103
105
  "LoopWrapper",
104
106
  "Mention",
@@ -63,15 +63,13 @@ class InlineButton(BaseButton):
63
63
  url: str | None = None
64
64
  login_url: dict[str, typing.Any] | LoginUrl | None = None
65
65
  pay: bool | None = None
66
- callback_data: (
67
- str | dict[str, typing.Any] | DataclassInstance | msgspec.Struct | None
68
- ) = None
66
+ callback_data: str | dict[str, typing.Any] | DataclassInstance | msgspec.Struct | None = None
69
67
  callback_game: dict[str, typing.Any] | CallbackGame | None = None
70
68
  switch_inline_query: str | None = None
71
69
  switch_inline_query_current_chat: str | None = None
72
- switch_inline_query_chosen_chat: (
73
- dict[str, typing.Any] | SwitchInlineQueryChosenChat | None
74
- ) = None
70
+ switch_inline_query_chosen_chat: dict[str, typing.Any] | SwitchInlineQueryChosenChat | None = (
71
+ None
72
+ )
75
73
  web_app: dict[str, typing.Any] | WebAppInfo | None = None
76
74
 
77
75
 
@@ -16,9 +16,7 @@ class ABCErrorHandler(ABC, typing.Generic[EventT]):
16
16
  self,
17
17
  *args: typing.Any,
18
18
  **kwargs: typing.Any,
19
- ) -> typing.Callable[
20
- [typing.Callable[..., typing.Any]], typing.Callable[..., typing.Any]
21
- ]:
19
+ ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Callable[..., typing.Any]]:
22
20
  """Decorator for registering callback as an error handler."""
23
21
 
24
22
  @abstractmethod
@@ -1,10 +1,7 @@
1
- import typing
2
-
3
-
4
- class CatcherError(TypeError):
5
- def __init__(self, exc: typing.Any, error: str) -> None:
1
+ class CatcherError(BaseException):
2
+ def __init__(self, exc: BaseException, message: str) -> None:
6
3
  self.exc = exc
7
- self.error = error
4
+ self.message = message
8
5
 
9
6
 
10
7
  __all__ = ("CatcherError",)
@@ -13,9 +13,7 @@ from .error import CatcherError
13
13
 
14
14
  F = typing.TypeVar("F", bound="FuncCatcher")
15
15
  ExceptionT = typing.TypeVar("ExceptionT", bound=BaseException, contravariant=True)
16
- FuncCatcher = typing.Callable[
17
- typing.Concatenate[ExceptionT, ...], typing.Awaitable[typing.Any]
18
- ]
16
+ FuncCatcher = typing.Callable[typing.Concatenate[ExceptionT, ...], typing.Awaitable[typing.Any]]
19
17
 
20
18
 
21
19
  @dataclasses.dataclass(frozen=True, repr=False)
@@ -59,8 +57,7 @@ class Catcher(typing.Generic[EventT]):
59
57
  ) -> Result[typing.Any, BaseException]:
60
58
  if self.match_exception(exception):
61
59
  logger.debug(
62
- "Catcher {!r} caught an exception in handler {!r}, "
63
- "running catcher...".format(
60
+ "Catcher {!r} caught an exception in handler {!r}, " "running catcher...".format(
64
61
  self.func.__name__,
65
62
  handler_name,
66
63
  )
@@ -109,7 +106,8 @@ class ErrorHandler(ABCErrorHandler[EventT]):
109
106
  ignore_errors: bool = False,
110
107
  ):
111
108
  """Register the catcher.
112
- :param logging: Error logging in stderr.
109
+
110
+ :param logging: Logging the result of the catcher at the level 'DEBUG'.
113
111
  :param raise_exception: Raise an exception if the catcher hasn't started.
114
112
  :param ignore_errors: Ignore errors that may occur.
115
113
  """
@@ -142,21 +140,21 @@ class ErrorHandler(ABCErrorHandler[EventT]):
142
140
  return Error(
143
141
  CatcherError(
144
142
  exc,
145
- "Exception {!r} was occurred during the running catcher {!r}.".format(
143
+ "Exception {} was occurred during the running catcher {!r}.".format(
146
144
  repr(exc), self.catcher.func.__name__
147
145
  ),
148
146
  )
149
147
  )
150
148
 
151
- def process_catcher_error(self, error: CatcherError) -> Result[None, str]:
149
+ def process_catcher_error(self, error: CatcherError) -> Result[None, BaseException]:
152
150
  assert self.catcher is not None
153
151
 
154
152
  if self.catcher.raise_exception:
155
153
  raise error.exc from None
156
- if not self.catcher.ignore_errors:
157
- return Error(error.error)
158
154
  if self.catcher.logging:
159
- logger.error(error.error)
155
+ logger.error(error.message)
156
+ if not self.catcher.ignore_errors:
157
+ return Error(error.exc)
160
158
 
161
159
  return Ok(None)
162
160
 
@@ -166,12 +164,18 @@ class ErrorHandler(ABCErrorHandler[EventT]):
166
164
  event: EventT,
167
165
  api: ABCAPI,
168
166
  ctx: Context,
169
- ) -> Result[typing.Any, typing.Any]:
167
+ ) -> Result[typing.Any, BaseException]:
170
168
  if not self.catcher:
171
169
  return Ok(await handler(event, **magic_bundle(handler, ctx))) # type: ignore
172
170
 
173
171
  match await self.process(handler, event, api, ctx):
174
- case Ok(_) as ok:
172
+ case Ok(value) as ok:
173
+ if self.catcher.logging:
174
+ logger.debug(
175
+ "Catcher {!r} returned a value: {!r}",
176
+ self.catcher.func.__name__,
177
+ value,
178
+ )
175
179
  return ok
176
180
  case Error(exc) as err:
177
181
  if isinstance(exc, CatcherError):
@@ -49,9 +49,7 @@ class StringFormatter(string.Formatter):
49
49
  )
50
50
  return fmt
51
51
 
52
- def get_spec_formatter(
53
- self, value: SpecialFormat
54
- ) -> typing.Callable[..., "TagFormat"]:
52
+ def get_spec_formatter(self, value: SpecialFormat) -> typing.Callable[..., "TagFormat"]:
55
53
  return globals()[value.__formatter_name__]
56
54
 
57
55
  def check_formats(self, value: typing.Any, fmts: list[str]) -> "TagFormat":
@@ -241,9 +239,7 @@ def start_bot_link(bot_id: str | int, data: str, string: str | None = None) -> T
241
239
  return link(get_start_bot_link(bot_id, data), string)
242
240
 
243
241
 
244
- def start_group_link(
245
- bot_id: str | int, data: str, string: str | None = None
246
- ) -> TagFormat:
242
+ def start_group_link(bot_id: str | int, data: str, string: str | None = None) -> TagFormat:
247
243
  return link(get_start_group_link(bot_id, data), string)
248
244
 
249
245
 
@@ -29,9 +29,7 @@ def get_invite_chat_link(invite_link: str) -> str:
29
29
 
30
30
 
31
31
  def user_open_message_link(user_id: int, message: str | None = None) -> str:
32
- return f"tg://openmessage?user_id={user_id}" + (
33
- "" if not message else f"&msg?text={message}"
34
- )
32
+ return f"tg://openmessage?user_id={user_id}" + ("" if not message else f"&msg?text={message}")
35
33
 
36
34
 
37
35
  __all__ = (
@@ -36,9 +36,7 @@ class GlobalCtxVar(typing.Generic[T]):
36
36
  @classmethod
37
37
  def collect(cls, name: str, ctx_value: T | CtxVariable[T]) -> typing.Self:
38
38
  ctx_value = (
39
- CtxVar(ctx_value)
40
- if not isinstance(ctx_value, CtxVar | GlobalCtxVar)
41
- else ctx_value
39
+ CtxVar(ctx_value) if not isinstance(ctx_value, CtxVar | GlobalCtxVar) else ctx_value
42
40
  )
43
41
  params = ctx_value.__dict__
44
42
  params["name"] = name
@@ -15,10 +15,8 @@ F = typing.TypeVar("F", bound=typing.Callable)
15
15
  CtxValueT = typing.TypeVar("CtxValueT", default=typing.Any)
16
16
 
17
17
  if typing.TYPE_CHECKING:
18
-
19
18
  _: typing.TypeAlias = None
20
19
  else:
21
-
22
20
  _ = lambda: None
23
21
 
24
22
 
@@ -51,14 +49,10 @@ def root_protection(func: F) -> F:
51
49
 
52
50
  @wraps(func)
53
51
  def wrapper(self: "GlobalContext", name: str, /, *args) -> typing.Any:
54
- if self.is_root_attribute(name) and name in (
55
- self.__dict__ | self.__class__.__dict__
56
- ):
52
+ if self.is_root_attribute(name) and name in (self.__dict__ | self.__class__.__dict__):
57
53
  root_attr = self.get_root_attribute(name).unwrap()
58
54
  if all((not root_attr.can_be_rewritten, not root_attr.can_be_read)):
59
- raise AttributeError(
60
- f"Unable to set, get, delete root attribute {name!r}."
61
- )
55
+ raise AttributeError(f"Unable to set, get, delete root attribute {name!r}.")
62
56
  if func.__name__ == "__setattr__" and not root_attr.can_be_rewritten:
63
57
  raise AttributeError(f"Unable to set root attribute {name!r}.")
64
58
  if func.__name__ == "__getattr__" and not root_attr.can_be_read:
@@ -106,9 +100,7 @@ class Storage:
106
100
  )
107
101
 
108
102
  def __repr__(self) -> str:
109
- return "<ContextStorage: %s>" % ", ".join(
110
- "ctx @" + repr(x) for x in self._storage
111
- )
103
+ return "<ContextStorage: %s>" % ", ".join("ctx @" + repr(x) for x in self._storage)
112
104
 
113
105
  @property
114
106
  def storage(self) -> dict[str, "GlobalContext"]:
@@ -132,7 +124,9 @@ class Storage:
132
124
  order_default=True,
133
125
  field_specifiers=(ctx_var,),
134
126
  )
135
- class GlobalContext(ABCGlobalContext, typing.Generic[CtxValueT], dict[str, GlobalCtxVar[CtxValueT]]):
127
+ class GlobalContext(
128
+ ABCGlobalContext, typing.Generic[CtxValueT], dict[str, GlobalCtxVar[CtxValueT]]
129
+ ):
136
130
  """GlobalContext.
137
131
 
138
132
  ```
@@ -208,19 +202,14 @@ class GlobalContext(ABCGlobalContext, typing.Generic[CtxValueT], dict[str, Globa
208
202
  """Returns True if the names of context stores
209
203
  that use self and __value instances are equivalent."""
210
204
 
211
- return (
212
- isinstance(__value, GlobalContext)
213
- and self.__ctx_name__ == __value.__ctx_name__
214
- )
205
+ return isinstance(__value, GlobalContext) and self.__ctx_name__ == __value.__ctx_name__
215
206
 
216
207
  def __setitem__(self, __name: str, __value: CtxValueT | CtxVariable[CtxValueT]):
217
208
  if is_dunder(__name):
218
209
  raise NameError("Cannot set a context variable with dunder name.")
219
210
  var = self.get(__name)
220
211
  if var and var.unwrap().const:
221
- raise TypeError(
222
- f"Unable to set variable {__name!r}, because it's a constant."
223
- )
212
+ raise TypeError(f"Unable to set variable {__name!r}, because it's a constant.")
224
213
  dict.__setitem__(self, __name, GlobalCtxVar.collect(__name, __value))
225
214
 
226
215
  def __getitem__(self, __name: str) -> CtxValueT:
@@ -229,9 +218,7 @@ class GlobalContext(ABCGlobalContext, typing.Generic[CtxValueT], dict[str, Globa
229
218
  def __delitem__(self, __name: str):
230
219
  var = self.get(__name).unwrap()
231
220
  if var.const:
232
- raise TypeError(
233
- f"Unable to delete variable {__name!r}, because it's a constant."
234
- )
221
+ raise TypeError(f"Unable to delete variable {__name!r}, because it's a constant.")
235
222
  dict.__delitem__(self, __name)
236
223
 
237
224
  @root_protection
@@ -328,9 +315,7 @@ class GlobalContext(ABCGlobalContext, typing.Generic[CtxValueT], dict[str, Globa
328
315
  var_value_type: type[T],
329
316
  ) -> Option[GlobalCtxVar[T]]: ...
330
317
 
331
- def pop(
332
- self, var_name: str, var_value_type: type[T] = object
333
- ) -> Option[GlobalCtxVar[T]]:
318
+ def pop(self, var_name: str, var_value_type: type[T] = object) -> Option[GlobalCtxVar[T]]:
334
319
  """Pop context variable by name."""
335
320
 
336
321
  val = self.get(var_name, var_value_type)
@@ -398,9 +383,7 @@ class GlobalContext(ABCGlobalContext, typing.Generic[CtxValueT], dict[str, Globa
398
383
 
399
384
  var = self.get(old_var_name).unwrap()
400
385
  if var.const:
401
- return Error(
402
- f"Unable to rename variable {old_var_name!r}, " "because it's a constant."
403
- )
386
+ return Error(f"Unable to rename variable {old_var_name!r}, " "because it's a constant.")
404
387
  del self[old_var_name]
405
388
  self[new_var_name] = var.value
406
389
  return Ok(_())
@@ -419,9 +402,8 @@ class GlobalContext(ABCGlobalContext, typing.Generic[CtxValueT], dict[str, Globa
419
402
  return dict.clear(self)
420
403
 
421
404
  for name, var in self.dict().items():
422
- if var.const:
423
- continue
424
- del self[name]
405
+ if not var.const:
406
+ del self[name]
425
407
 
426
408
  def delete_ctx(self) -> Result[_, str]:
427
409
  """Delete context by `ctx_name`."""
@@ -17,9 +17,7 @@ class ABCTranslatorMiddleware(ABCMiddleware[T]):
17
17
  pass
18
18
 
19
19
  async def pre(self, event: T, ctx: dict) -> bool:
20
- ctx[I18nEnum.I18N] = self.i18n.get_translator_by_locale(
21
- await self.get_locale(event)
22
- )
20
+ ctx[I18nEnum.I18N] = self.i18n.get_translator_by_locale(await self.get_locale(event))
23
21
  return True
24
22
 
25
23
 
@@ -0,0 +1,37 @@
1
+ import typing
2
+ from collections import UserDict, deque
3
+
4
+ KT = typing.TypeVar("KT")
5
+ VT = typing.TypeVar("VT")
6
+
7
+
8
+ class LimitedDict(UserDict[KT, VT]):
9
+ def __init__(self, *, maxlimit: int = 1000) -> None:
10
+ super().__init__()
11
+ self.maxlimit = maxlimit
12
+ self.queue: deque[KT] = deque(maxlen=maxlimit)
13
+
14
+ def set(self, key: KT, value: VT, /) -> VT | None:
15
+ """Set item in the dictionary.
16
+ Returns a value that was deleted when the limit in the dictionary
17
+ was reached, otherwise None.
18
+ """
19
+
20
+ deleted_item = None
21
+ if len(self.queue) >= self.maxlimit:
22
+ deleted_item = self.pop(self.queue.popleft(), None)
23
+ if key not in self.queue:
24
+ self.queue.append(key)
25
+ super().__setitem__(key, value)
26
+ return deleted_item
27
+
28
+ def __setitem__(self, key: KT, value: VT, /) -> None:
29
+ self.set(key, value)
30
+
31
+ def __delitem__(self, key: KT) -> None:
32
+ if key in self.queue:
33
+ self.queue.remove(key)
34
+ return super().__delitem__(key)
35
+
36
+
37
+ __all__ = ("LimitedDict",)
@@ -17,7 +17,7 @@ Task: typing.TypeAlias = typing.Union[CoroutineFunc, CoroutineTask, "DelayedTask
17
17
 
18
18
 
19
19
  def run_tasks(
20
- tasks: list[CoroutineTask[typing.Any]],
20
+ tasks: list[CoroutineTask[typing.Any]],
21
21
  loop: asyncio.AbstractEventLoop,
22
22
  ) -> None:
23
23
  while tasks:
@@ -59,12 +59,8 @@ class DelayedTask(typing.Generic[CoroFunc]):
59
59
 
60
60
  @dataclasses.dataclass(kw_only=True)
61
61
  class Lifespan:
62
- startup_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(
63
- default_factory=lambda: []
64
- )
65
- shutdown_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(
66
- default_factory=lambda: []
67
- )
62
+ startup_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(default_factory=lambda: [])
63
+ shutdown_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(default_factory=lambda: [])
68
64
 
69
65
  def on_startup(self, task_or_func: Task) -> Task:
70
66
  task_or_func = to_coroutine_task(task_or_func)