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.

Files changed (169) hide show
  1. telegrinder/__init__.py +30 -31
  2. telegrinder/api/__init__.py +2 -1
  3. telegrinder/api/api.py +28 -20
  4. telegrinder/api/error.py +8 -4
  5. telegrinder/api/response.py +2 -2
  6. telegrinder/api/token.py +2 -2
  7. telegrinder/bot/__init__.py +6 -0
  8. telegrinder/bot/bot.py +38 -31
  9. telegrinder/bot/cute_types/__init__.py +2 -0
  10. telegrinder/bot/cute_types/base.py +54 -128
  11. telegrinder/bot/cute_types/callback_query.py +76 -61
  12. telegrinder/bot/cute_types/chat_join_request.py +4 -3
  13. telegrinder/bot/cute_types/chat_member_updated.py +28 -31
  14. telegrinder/bot/cute_types/inline_query.py +5 -4
  15. telegrinder/bot/cute_types/message.py +555 -602
  16. telegrinder/bot/cute_types/pre_checkout_query.py +42 -0
  17. telegrinder/bot/cute_types/update.py +20 -12
  18. telegrinder/bot/cute_types/utils.py +3 -36
  19. telegrinder/bot/dispatch/__init__.py +4 -0
  20. telegrinder/bot/dispatch/abc.py +8 -9
  21. telegrinder/bot/dispatch/context.py +5 -7
  22. telegrinder/bot/dispatch/dispatch.py +85 -33
  23. telegrinder/bot/dispatch/handler/abc.py +5 -6
  24. telegrinder/bot/dispatch/handler/audio_reply.py +2 -2
  25. telegrinder/bot/dispatch/handler/base.py +3 -3
  26. telegrinder/bot/dispatch/handler/document_reply.py +2 -2
  27. telegrinder/bot/dispatch/handler/func.py +36 -42
  28. telegrinder/bot/dispatch/handler/media_group_reply.py +5 -4
  29. telegrinder/bot/dispatch/handler/message_reply.py +2 -2
  30. telegrinder/bot/dispatch/handler/photo_reply.py +2 -2
  31. telegrinder/bot/dispatch/handler/sticker_reply.py +2 -2
  32. telegrinder/bot/dispatch/handler/video_reply.py +2 -2
  33. telegrinder/bot/dispatch/middleware/abc.py +83 -8
  34. telegrinder/bot/dispatch/middleware/global_middleware.py +70 -0
  35. telegrinder/bot/dispatch/process.py +44 -50
  36. telegrinder/bot/dispatch/return_manager/__init__.py +2 -0
  37. telegrinder/bot/dispatch/return_manager/abc.py +6 -10
  38. telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +20 -0
  39. telegrinder/bot/dispatch/view/__init__.py +2 -0
  40. telegrinder/bot/dispatch/view/abc.py +10 -6
  41. telegrinder/bot/dispatch/view/base.py +81 -50
  42. telegrinder/bot/dispatch/view/box.py +20 -9
  43. telegrinder/bot/dispatch/view/callback_query.py +3 -4
  44. telegrinder/bot/dispatch/view/chat_join_request.py +2 -7
  45. telegrinder/bot/dispatch/view/chat_member.py +3 -5
  46. telegrinder/bot/dispatch/view/inline_query.py +3 -4
  47. telegrinder/bot/dispatch/view/message.py +3 -4
  48. telegrinder/bot/dispatch/view/pre_checkout_query.py +16 -0
  49. telegrinder/bot/dispatch/view/raw.py +42 -40
  50. telegrinder/bot/dispatch/waiter_machine/actions.py +5 -4
  51. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +0 -0
  52. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +0 -0
  53. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +9 -7
  54. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +0 -0
  55. telegrinder/bot/dispatch/waiter_machine/hasher/state.py +3 -2
  56. telegrinder/bot/dispatch/waiter_machine/machine.py +113 -34
  57. telegrinder/bot/dispatch/waiter_machine/middleware.py +15 -10
  58. telegrinder/bot/dispatch/waiter_machine/short_state.py +7 -18
  59. telegrinder/bot/polling/polling.py +62 -54
  60. telegrinder/bot/rules/__init__.py +24 -1
  61. telegrinder/bot/rules/abc.py +17 -10
  62. telegrinder/bot/rules/callback_data.py +20 -61
  63. telegrinder/bot/rules/chat_join.py +6 -4
  64. telegrinder/bot/rules/command.py +4 -4
  65. telegrinder/bot/rules/enum_text.py +1 -4
  66. telegrinder/bot/rules/func.py +5 -3
  67. telegrinder/bot/rules/fuzzy.py +1 -1
  68. telegrinder/bot/rules/id.py +24 -0
  69. telegrinder/bot/rules/inline.py +6 -4
  70. telegrinder/bot/rules/integer.py +2 -1
  71. telegrinder/bot/rules/logic.py +18 -0
  72. telegrinder/bot/rules/markup.py +5 -6
  73. telegrinder/bot/rules/message.py +2 -4
  74. telegrinder/bot/rules/message_entities.py +1 -3
  75. telegrinder/bot/rules/node.py +15 -9
  76. telegrinder/bot/rules/payload.py +81 -0
  77. telegrinder/bot/rules/payment_invoice.py +29 -0
  78. telegrinder/bot/rules/regex.py +5 -6
  79. telegrinder/bot/rules/state.py +1 -3
  80. telegrinder/bot/rules/text.py +10 -5
  81. telegrinder/bot/rules/update.py +0 -0
  82. telegrinder/bot/scenario/abc.py +2 -4
  83. telegrinder/bot/scenario/checkbox.py +12 -14
  84. telegrinder/bot/scenario/choice.py +6 -9
  85. telegrinder/client/__init__.py +9 -1
  86. telegrinder/client/abc.py +35 -10
  87. telegrinder/client/aiohttp.py +28 -24
  88. telegrinder/client/form_data.py +31 -0
  89. telegrinder/client/sonic.py +212 -0
  90. telegrinder/model.py +38 -145
  91. telegrinder/modules.py +3 -1
  92. telegrinder/msgspec_utils.py +136 -68
  93. telegrinder/node/__init__.py +74 -13
  94. telegrinder/node/attachment.py +92 -16
  95. telegrinder/node/base.py +196 -68
  96. telegrinder/node/callback_query.py +17 -16
  97. telegrinder/node/command.py +3 -2
  98. telegrinder/node/composer.py +40 -75
  99. telegrinder/node/container.py +13 -7
  100. telegrinder/node/either.py +82 -0
  101. telegrinder/node/event.py +20 -31
  102. telegrinder/node/file.py +51 -0
  103. telegrinder/node/me.py +4 -5
  104. telegrinder/node/payload.py +78 -0
  105. telegrinder/node/polymorphic.py +27 -8
  106. telegrinder/node/rule.py +2 -6
  107. telegrinder/node/scope.py +4 -6
  108. telegrinder/node/source.py +37 -21
  109. telegrinder/node/text.py +20 -8
  110. telegrinder/node/tools/generator.py +7 -11
  111. telegrinder/py.typed +0 -0
  112. telegrinder/rules.py +0 -61
  113. telegrinder/tools/__init__.py +97 -38
  114. telegrinder/tools/adapter/__init__.py +19 -0
  115. telegrinder/tools/adapter/abc.py +49 -0
  116. telegrinder/tools/adapter/dataclass.py +56 -0
  117. telegrinder/{bot/rules → tools}/adapter/event.py +8 -10
  118. telegrinder/{bot/rules → tools}/adapter/node.py +8 -10
  119. telegrinder/{bot/rules → tools}/adapter/raw_event.py +2 -2
  120. telegrinder/{bot/rules → tools}/adapter/raw_update.py +2 -2
  121. telegrinder/tools/buttons.py +52 -26
  122. telegrinder/tools/callback_data_serilization/__init__.py +5 -0
  123. telegrinder/tools/callback_data_serilization/abc.py +51 -0
  124. telegrinder/tools/callback_data_serilization/json_ser.py +60 -0
  125. telegrinder/tools/callback_data_serilization/msgpack_ser.py +172 -0
  126. telegrinder/tools/error_handler/abc.py +4 -7
  127. telegrinder/tools/error_handler/error.py +0 -0
  128. telegrinder/tools/error_handler/error_handler.py +34 -48
  129. telegrinder/tools/formatting/__init__.py +57 -37
  130. telegrinder/tools/formatting/deep_links.py +541 -0
  131. telegrinder/tools/formatting/{html.py → html_formatter.py} +51 -79
  132. telegrinder/tools/formatting/spec_html_formats.py +14 -60
  133. telegrinder/tools/functional.py +1 -5
  134. telegrinder/tools/global_context/global_context.py +26 -51
  135. telegrinder/tools/global_context/telegrinder_ctx.py +3 -3
  136. telegrinder/tools/i18n/abc.py +0 -0
  137. telegrinder/tools/i18n/middleware/abc.py +3 -6
  138. telegrinder/tools/input_file_directory.py +30 -0
  139. telegrinder/tools/keyboard.py +9 -9
  140. telegrinder/tools/lifespan.py +105 -0
  141. telegrinder/tools/limited_dict.py +5 -10
  142. telegrinder/tools/loop_wrapper/abc.py +7 -2
  143. telegrinder/tools/loop_wrapper/loop_wrapper.py +40 -95
  144. telegrinder/tools/magic.py +184 -34
  145. telegrinder/tools/state_storage/__init__.py +0 -0
  146. telegrinder/tools/state_storage/abc.py +5 -9
  147. telegrinder/tools/state_storage/memory.py +1 -1
  148. telegrinder/tools/strings.py +13 -0
  149. telegrinder/types/__init__.py +8 -0
  150. telegrinder/types/enums.py +31 -21
  151. telegrinder/types/input_file.py +51 -0
  152. telegrinder/types/methods.py +531 -109
  153. telegrinder/types/objects.py +934 -826
  154. telegrinder/verification_utils.py +0 -2
  155. {telegrinder-0.3.4.post1.dist-info → telegrinder-0.4.0.dist-info}/LICENSE +2 -2
  156. telegrinder-0.4.0.dist-info/METADATA +144 -0
  157. telegrinder-0.4.0.dist-info/RECORD +182 -0
  158. {telegrinder-0.3.4.post1.dist-info → telegrinder-0.4.0.dist-info}/WHEEL +1 -1
  159. telegrinder/bot/rules/adapter/__init__.py +0 -17
  160. telegrinder/bot/rules/adapter/abc.py +0 -31
  161. telegrinder/node/message.py +0 -14
  162. telegrinder/node/update.py +0 -15
  163. telegrinder/tools/formatting/links.py +0 -38
  164. telegrinder/tools/kb_set/__init__.py +0 -4
  165. telegrinder/tools/kb_set/base.py +0 -15
  166. telegrinder/tools/kb_set/yaml.py +0 -63
  167. telegrinder-0.3.4.post1.dist-info/METADATA +0 -110
  168. telegrinder-0.3.4.post1.dist-info/RECORD +0 -165
  169. /telegrinder/{bot/rules → tools}/adapter/errors.py +0 -0
@@ -1,5 +1,4 @@
1
1
  import re
2
- import typing
3
2
 
4
3
  import vbml
5
4
 
@@ -16,12 +15,13 @@ class TelegrinderContext(GlobalContext):
16
15
  ctx1 = TelegrinderContext()
17
16
  ctx2 = GlobalContext("telegrinder") # same, but without the type-hints
18
17
  assert ctx1 == ctx2 # ok
19
- ```"""
18
+ ```
19
+ """
20
20
 
21
21
  __ctx_name__ = "telegrinder"
22
22
 
23
23
  vbml_pattern_flags: re.RegexFlag | None = None
24
- vbml_patcher: typing.ClassVar[vbml.Patcher] = ctx_var(vbml.Patcher(), const=True)
24
+ vbml_patcher: vbml.Patcher = ctx_var(default=vbml.Patcher(), frozen=True)
25
25
 
26
26
 
27
27
  __all__ = ("TelegrinderContext",)
File without changes
@@ -1,4 +1,3 @@
1
- import typing
2
1
  from abc import abstractmethod
3
2
 
4
3
  from telegrinder.bot.cute_types.base import BaseCute
@@ -6,18 +5,16 @@ from telegrinder.bot.dispatch.context import Context
6
5
  from telegrinder.bot.dispatch.middleware import ABCMiddleware
7
6
  from telegrinder.tools.i18n import ABCI18n, I18nEnum
8
7
 
9
- T = typing.TypeVar("T", bound=BaseCute)
10
8
 
11
-
12
- class ABCTranslatorMiddleware(ABCMiddleware[T]):
9
+ class ABCTranslatorMiddleware[Event: BaseCute](ABCMiddleware[Event]):
13
10
  def __init__(self, i18n: ABCI18n) -> None:
14
11
  self.i18n = i18n
15
12
 
16
13
  @abstractmethod
17
- async def get_locale(self, event: T) -> str:
14
+ async def get_locale(self, event: Event) -> str:
18
15
  pass
19
16
 
20
- async def pre(self, event: T, ctx: Context) -> bool:
17
+ async def pre(self, event: Event, ctx: Context) -> bool:
21
18
  ctx[I18nEnum.I18N] = self.i18n.get_translator_by_locale(await self.get_locale(event))
22
19
  return True
23
20
 
@@ -0,0 +1,30 @@
1
+ import dataclasses
2
+ import pathlib
3
+
4
+ from telegrinder.types.objects import InputFile
5
+
6
+
7
+ @dataclasses.dataclass
8
+ class InputFileDirectory:
9
+ directory: pathlib.Path
10
+ storage: dict[str, InputFile] = dataclasses.field(init=False, repr=False)
11
+
12
+ def __post_init__(self) -> None:
13
+ self.storage = self._load_files()
14
+
15
+ def _load_files(self) -> dict[str, InputFile]:
16
+ files = {}
17
+
18
+ for path in self.directory.rglob("*"):
19
+ if path.is_file():
20
+ relative_path = path.relative_to(self.directory)
21
+ files[str(relative_path)] = InputFile(path.name, path.read_bytes())
22
+
23
+ return files
24
+
25
+ def get(self, filename: str, /) -> InputFile:
26
+ assert filename in self.storage, f"File {filename!r} not found."
27
+ return self.storage[filename]
28
+
29
+
30
+ __all__ = ("InputFileDirectory",)
@@ -1,20 +1,20 @@
1
1
  import dataclasses
2
2
  import typing
3
3
  from abc import ABC, abstractmethod
4
- from types import NoneType
5
4
 
6
- from fntypes.option import Nothing, Some
5
+ from fntypes.option import Some
7
6
 
7
+ from telegrinder.model import is_none
8
8
  from telegrinder.types.objects import (
9
9
  InlineKeyboardMarkup,
10
10
  ReplyKeyboardMarkup,
11
11
  ReplyKeyboardRemove,
12
12
  )
13
13
 
14
- from .buttons import Button, InlineButton, KeyboardButton, RowButtons
14
+ from .buttons import BaseButton, Button, InlineButton, RowButtons
15
15
 
16
- DictStrAny: typing.TypeAlias = dict[str, typing.Any]
17
- AnyMarkup: typing.TypeAlias = InlineKeyboardMarkup | ReplyKeyboardMarkup
16
+ type DictStrAny = dict[str, typing.Any]
17
+ type AnyMarkup = InlineKeyboardMarkup | ReplyKeyboardMarkup
18
18
 
19
19
 
20
20
  def copy_keyboard(keyboard: list[list[DictStrAny]]) -> list[list[DictStrAny]]:
@@ -30,7 +30,7 @@ class KeyboardModel:
30
30
  keyboard: list[list[DictStrAny]]
31
31
 
32
32
 
33
- class ABCMarkup(ABC, typing.Generic[KeyboardButton]):
33
+ class ABCMarkup[KeyboardButton: BaseButton](ABC):
34
34
  BUTTON: type[KeyboardButton]
35
35
  keyboard: list[list[DictStrAny]]
36
36
 
@@ -46,8 +46,8 @@ class ABCMarkup(ABC, typing.Generic[KeyboardButton]):
46
46
  def get_empty_markup(cls) -> AnyMarkup:
47
47
  return cls().get_markup()
48
48
 
49
- def add(self, row_or_button: RowButtons[KeyboardButton] | KeyboardButton) -> typing.Self:
50
- if not len(self.keyboard):
49
+ def add(self, row_or_button: RowButtons[KeyboardButton] | KeyboardButton, /) -> typing.Self:
50
+ if not self.keyboard:
51
51
  self.row()
52
52
 
53
53
  if isinstance(row_or_button, RowButtons):
@@ -99,7 +99,7 @@ class Keyboard(ABCMarkup[Button], KeyboardModel):
99
99
  return {
100
100
  k: v.unwrap() if v and isinstance(v, Some) else v
101
101
  for k, v in dataclasses.asdict(self).items()
102
- if type(v) not in (NoneType, Nothing)
102
+ if not is_none(v)
103
103
  }
104
104
 
105
105
  def get_markup(self) -> ReplyKeyboardMarkup:
@@ -0,0 +1,105 @@
1
+ import asyncio
2
+ import dataclasses
3
+ import datetime
4
+ import typing
5
+
6
+ type CoroutineTask[T] = typing.Coroutine[typing.Any, typing.Any, T]
7
+ type CoroutineFunc[**P, T] = typing.Callable[P, CoroutineTask[T]]
8
+ type Task[**P, T] = CoroutineFunc[P, T] | CoroutineTask[T] | DelayedTask[typing.Callable[P, CoroutineTask[T]]]
9
+
10
+
11
+ def run_tasks(
12
+ tasks: list[CoroutineTask[typing.Any]],
13
+ /,
14
+ ) -> None:
15
+ loop = asyncio.get_event_loop()
16
+ while tasks:
17
+ loop.run_until_complete(tasks.pop(0))
18
+
19
+
20
+ def to_coroutine_task[**P, T](task: Task[P, T], /) -> CoroutineTask[T]:
21
+ if asyncio.iscoroutinefunction(task) or isinstance(task, DelayedTask):
22
+ task = task()
23
+ elif not asyncio.iscoroutine(task):
24
+ raise TypeError("Task should be coroutine or coroutine function.")
25
+ return task
26
+
27
+
28
+ @dataclasses.dataclass(slots=True)
29
+ class DelayedTask[Handler: CoroutineFunc[..., typing.Any]]:
30
+ handler: Handler
31
+ seconds: float | datetime.timedelta
32
+ repeat: bool = dataclasses.field(default=False, kw_only=True)
33
+ _cancelled: bool = dataclasses.field(default=False, init=False, repr=False)
34
+
35
+ @property
36
+ def is_cancelled(self) -> bool:
37
+ return self._cancelled
38
+
39
+ @property
40
+ def delay(self) -> float:
41
+ return self.seconds if isinstance(self.seconds, int | float) else self.seconds.total_seconds()
42
+
43
+ async def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> typing.Any:
44
+ while not self.is_cancelled:
45
+ await asyncio.sleep(self.delay)
46
+ if self.is_cancelled:
47
+ break
48
+ try:
49
+ await self.handler(*args, **kwargs)
50
+ finally:
51
+ if not self.repeat:
52
+ break
53
+
54
+ def cancel(self) -> None:
55
+ if not self._cancelled:
56
+ self._cancelled = True
57
+
58
+
59
+ @dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
60
+ class Lifespan:
61
+ startup_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(default_factory=lambda: [])
62
+ shutdown_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(default_factory=lambda: [])
63
+
64
+ def on_startup[**P, T](self, task: Task[P, T], /) -> Task[P, T]:
65
+ self.startup_tasks.append(to_coroutine_task(task))
66
+ return task
67
+
68
+ def on_shutdown[**P, T](self, task: Task[P, T], /) -> Task[P, T]:
69
+ self.shutdown_tasks.append(to_coroutine_task(task))
70
+ return task
71
+
72
+ def start(self) -> None:
73
+ run_tasks(self.startup_tasks)
74
+
75
+ def shutdown(self) -> None:
76
+ run_tasks(self.shutdown_tasks)
77
+
78
+ def __enter__(self) -> None:
79
+ self.start()
80
+
81
+ def __exit__(self) -> None:
82
+ self.shutdown()
83
+
84
+ async def __aenter__(self) -> None:
85
+ for task in self.startup_tasks:
86
+ await task
87
+
88
+ async def __aexit__(self, *args) -> None:
89
+ for task in self.shutdown_tasks:
90
+ await task
91
+
92
+ def __add__(self, other: typing.Self, /) -> typing.Self:
93
+ return self.__class__(
94
+ startup_tasks=self.startup_tasks + other.startup_tasks,
95
+ shutdown_tasks=self.shutdown_tasks + other.shutdown_tasks,
96
+ )
97
+
98
+
99
+ __all__ = (
100
+ "CoroutineTask",
101
+ "DelayedTask",
102
+ "Lifespan",
103
+ "run_tasks",
104
+ "to_coroutine_task",
105
+ )
@@ -1,22 +1,17 @@
1
- import typing
2
1
  from collections import UserDict, deque
3
2
 
4
- KT = typing.TypeVar("KT")
5
- VT = typing.TypeVar("VT")
6
3
 
7
-
8
- class LimitedDict(UserDict[KT, VT]):
4
+ class LimitedDict[Key, Value](UserDict[Key, Value]):
9
5
  def __init__(self, *, maxlimit: int = 1000) -> None:
10
6
  super().__init__()
11
7
  self.maxlimit = maxlimit
12
- self.queue: deque[KT] = deque(maxlen=maxlimit)
8
+ self.queue: deque[Key] = deque(maxlen=maxlimit)
13
9
 
14
- def set(self, key: KT, value: VT, /) -> VT | None:
10
+ def set(self, key: Key, value: Value, /) -> Value | None:
15
11
  """Set item in the dictionary.
16
12
  Returns a value that was deleted when the limit in the dictionary
17
13
  was reached, otherwise None.
18
14
  """
19
-
20
15
  deleted_item = None
21
16
  if len(self.queue) >= self.maxlimit:
22
17
  deleted_item = self.pop(self.queue.popleft(), None)
@@ -25,10 +20,10 @@ class LimitedDict(UserDict[KT, VT]):
25
20
  super().__setitem__(key, value)
26
21
  return deleted_item
27
22
 
28
- def __setitem__(self, key: KT, value: VT, /) -> None:
23
+ def __setitem__(self, key: Key, value: Value, /) -> None:
29
24
  self.set(key, value)
30
25
 
31
- def __delitem__(self, key: KT) -> None:
26
+ def __delitem__(self, key: Key) -> None:
32
27
  if key in self.queue:
33
28
  self.queue.remove(key)
34
29
  return super().__delitem__(key)
@@ -3,13 +3,18 @@ from abc import ABC, abstractmethod
3
3
 
4
4
 
5
5
  class ABCLoopWrapper(ABC):
6
+ @property
6
7
  @abstractmethod
7
- def add_task(self, task: typing.Any) -> None:
8
+ def is_running(self) -> bool:
8
9
  pass
9
10
 
10
11
  @abstractmethod
11
- def run_event_loop(self) -> None:
12
+ def add_task(self, task: typing.Any, /) -> None:
12
13
  pass
13
14
 
15
+ @abstractmethod
16
+ def run_event_loop(self) -> typing.NoReturn:
17
+ raise NotImplementedError
18
+
14
19
 
15
20
  __all__ = ("ABCLoopWrapper",)
@@ -1,84 +1,19 @@
1
1
  import asyncio
2
2
  import contextlib
3
- import dataclasses
4
3
  import datetime
5
4
  import typing
6
5
 
7
6
  from telegrinder.modules import logger
8
- from telegrinder.tools.loop_wrapper.abc import ABCLoopWrapper
9
-
10
- T = typing.TypeVar("T")
11
- P = typing.ParamSpec("P")
12
- CoroFunc = typing.TypeVar("CoroFunc", bound="CoroutineFunc[..., typing.Any]")
13
-
14
- CoroutineTask: typing.TypeAlias = typing.Coroutine[typing.Any, typing.Any, T]
15
- CoroutineFunc: typing.TypeAlias = typing.Callable[P, CoroutineTask[T]]
16
- Task: typing.TypeAlias = (
17
- "CoroutineFunc[P, T] | CoroutineTask[T] | DelayedTask[typing.Callable[P, CoroutineTask[T]]]"
7
+ from telegrinder.tools.lifespan import (
8
+ CoroutineFunc,
9
+ CoroutineTask,
10
+ DelayedTask,
11
+ Lifespan,
12
+ Task,
13
+ to_coroutine_task,
18
14
  )
19
-
20
-
21
- def run_tasks(
22
- tasks: list[CoroutineTask[typing.Any]],
23
- loop: asyncio.AbstractEventLoop,
24
- ) -> None:
25
- while tasks:
26
- loop.run_until_complete(tasks.pop(0))
27
-
28
-
29
- def to_coroutine_task(task: Task) -> CoroutineTask[typing.Any]:
30
- if asyncio.iscoroutinefunction(task) or isinstance(task, DelayedTask):
31
- task = task()
32
- elif not asyncio.iscoroutine(task):
33
- raise TypeError("Task should be coroutine or coroutine function.")
34
- return task
35
-
36
-
37
- @dataclasses.dataclass(slots=True)
38
- class DelayedTask(typing.Generic[CoroFunc]):
39
- handler: CoroFunc
40
- seconds: float
41
- repeat: bool = dataclasses.field(default=False, kw_only=True)
42
- _cancelled: bool = dataclasses.field(default=False, init=False, repr=False)
43
-
44
- @property
45
- def is_cancelled(self) -> bool:
46
- return self._cancelled
47
-
48
- async def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
49
- while not self.is_cancelled:
50
- await asyncio.sleep(self.seconds)
51
- if self.is_cancelled:
52
- break
53
- try:
54
- await self.handler(*args, **kwargs)
55
- finally:
56
- if not self.repeat:
57
- break
58
-
59
- def cancel(self) -> None:
60
- if not self._cancelled:
61
- self._cancelled = True
62
-
63
-
64
- @dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
65
- class Lifespan:
66
- startup_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(default_factory=lambda: [])
67
- shutdown_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(default_factory=lambda: [])
68
-
69
- def on_startup(self, task: Task, /) -> Task:
70
- self.startup_tasks.append(to_coroutine_task(task))
71
- return task
72
-
73
- def on_shutdown(self, task: Task, /) -> Task:
74
- self.shutdown_tasks.append(to_coroutine_task(task))
75
- return task
76
-
77
- def start(self, loop: asyncio.AbstractEventLoop, /) -> None:
78
- run_tasks(self.startup_tasks, loop)
79
-
80
- def shutdown(self, loop: asyncio.AbstractEventLoop, /) -> None:
81
- run_tasks(self.shutdown_tasks, loop)
15
+ from telegrinder.tools.loop_wrapper.abc import ABCLoopWrapper
16
+ from telegrinder.tools.magic import cancel_future
82
17
 
83
18
 
84
19
  class LoopWrapper(ABCLoopWrapper):
@@ -87,15 +22,20 @@ class LoopWrapper(ABCLoopWrapper):
87
22
  *,
88
23
  tasks: list[CoroutineTask[typing.Any]] | None = None,
89
24
  lifespan: Lifespan | None = None,
90
- event_loop: asyncio.AbstractEventLoop | None = None,
91
25
  ) -> None:
92
26
  self.tasks: list[CoroutineTask[typing.Any]] = tasks or []
93
27
  self.lifespan = lifespan or Lifespan()
94
- self._loop = event_loop
28
+ self._loop: asyncio.AbstractEventLoop | None = None
29
+
30
+ @property
31
+ def is_running(self) -> bool:
32
+ if self._loop is None:
33
+ return False
34
+ return self._loop.is_running()
95
35
 
96
36
  @property
97
37
  def loop(self) -> asyncio.AbstractEventLoop:
98
- assert self._loop is not None
38
+ assert self._loop is not None, "Loop is not set."
99
39
  return self._loop
100
40
 
101
41
  def __repr__(self) -> str:
@@ -106,15 +46,22 @@ class LoopWrapper(ABCLoopWrapper):
106
46
  self.lifespan,
107
47
  )
108
48
 
109
- def run_event_loop(self) -> None:
49
+ async def _run_tasks(self) -> None:
50
+ async with asyncio.TaskGroup() as tg:
51
+ while self.tasks:
52
+ tg.create_task(self.tasks.pop(0))
53
+
54
+ def run_event_loop(self) -> typing.NoReturn: # type: ignore
110
55
  if not self.tasks:
111
- logger.warning("You run loop with 0 tasks!")
56
+ logger.warning("Run loop without tasks!")
112
57
 
113
- self._loop = asyncio.new_event_loop() if self._loop is None else self._loop
114
- self.lifespan.start(self._loop)
58
+ try:
59
+ self._loop = asyncio.get_running_loop()
60
+ except RuntimeError:
61
+ self._loop = asyncio.get_event_loop()
115
62
 
116
- while self.tasks:
117
- self._loop.create_task(self.tasks.pop(0))
63
+ self.lifespan.start()
64
+ self._loop.create_task(self._run_tasks())
118
65
 
119
66
  tasks = asyncio.all_tasks(self._loop)
120
67
  try:
@@ -125,19 +72,19 @@ class LoopWrapper(ABCLoopWrapper):
125
72
  for task_result in tasks_results:
126
73
  try:
127
74
  task_result.result()
128
- except BaseException as ex:
129
- logger.exception(ex)
75
+ except BaseException:
76
+ logger.exception("Traceback message below:")
130
77
  tasks = asyncio.all_tasks(self._loop)
131
78
  except KeyboardInterrupt:
132
79
  print() # blank print for ^C
133
80
  logger.info("Caught KeyboardInterrupt, cancellation...")
134
81
  self.complete_tasks(tasks)
135
82
  finally:
136
- self.lifespan.shutdown(self._loop)
83
+ self.lifespan.shutdown()
137
84
  if self._loop.is_running():
138
85
  self._loop.close()
139
86
 
140
- def add_task(self, task: Task) -> None:
87
+ def add_task(self, task: Task[..., typing.Any], /) -> None:
141
88
  task = to_coroutine_task(task)
142
89
 
143
90
  if self._loop is not None and self._loop.is_running():
@@ -145,12 +92,10 @@ class LoopWrapper(ABCLoopWrapper):
145
92
  else:
146
93
  self.tasks.append(task)
147
94
 
148
- def complete_tasks(self, tasks: set[asyncio.Task[typing.Any]]) -> None:
95
+ def complete_tasks(self, tasks: set[asyncio.Task[typing.Any]], /) -> None:
149
96
  tasks = tasks | asyncio.all_tasks(self.loop)
150
- task_to_cancel = asyncio.gather(*tasks, return_exceptions=True)
151
- task_to_cancel.cancel()
152
- with contextlib.suppress(asyncio.CancelledError):
153
- self.loop.run_until_complete(task_to_cancel)
97
+ with contextlib.suppress(asyncio.CancelledError, asyncio.InvalidStateError):
98
+ self.loop.run_until_complete(cancel_future(asyncio.gather(*tasks, return_exceptions=True)))
154
99
 
155
100
  @typing.overload
156
101
  def timer(self, *, seconds: datetime.timedelta) -> typing.Callable[..., typing.Any]: ...
@@ -180,7 +125,7 @@ class LoopWrapper(ABCLoopWrapper):
180
125
  seconds += hours * 60 * 60
181
126
  seconds += days * 24 * 60 * 60
182
127
 
183
- def decorator(func: CoroFunc) -> CoroFunc:
128
+ def decorator[Func: CoroutineFunc[..., typing.Any]](func: Func) -> Func:
184
129
  self.add_task(DelayedTask(func, seconds, repeat=False))
185
130
  return func
186
131
 
@@ -214,11 +159,11 @@ class LoopWrapper(ABCLoopWrapper):
214
159
  seconds += hours * 60 * 60
215
160
  seconds += days * 24 * 60 * 60
216
161
 
217
- def decorator(func: CoroFunc) -> CoroFunc:
162
+ def decorator[Func: CoroutineFunc[..., typing.Any]](func: Func) -> Func:
218
163
  self.add_task(DelayedTask(func, seconds, repeat=True))
219
164
  return func
220
165
 
221
166
  return decorator
222
167
 
223
168
 
224
- __all__ = ("DelayedTask", "Lifespan", "LoopWrapper", "to_coroutine_task")
169
+ __all__ = ("DelayedTask", "LoopWrapper", "to_coroutine_task")