telegrinder 1.0.0rc1__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.
Files changed (215) hide show
  1. telegrinder/__init__.py +258 -0
  2. telegrinder/__meta__.py +1 -0
  3. telegrinder/api/__init__.py +15 -0
  4. telegrinder/api/api.py +175 -0
  5. telegrinder/api/error.py +50 -0
  6. telegrinder/api/response.py +23 -0
  7. telegrinder/api/token.py +30 -0
  8. telegrinder/api/validators.py +30 -0
  9. telegrinder/bot/__init__.py +144 -0
  10. telegrinder/bot/bot.py +70 -0
  11. telegrinder/bot/cute_types/__init__.py +41 -0
  12. telegrinder/bot/cute_types/base.py +228 -0
  13. telegrinder/bot/cute_types/base.pyi +49 -0
  14. telegrinder/bot/cute_types/business_connection.py +9 -0
  15. telegrinder/bot/cute_types/business_messages_deleted.py +9 -0
  16. telegrinder/bot/cute_types/callback_query.py +248 -0
  17. telegrinder/bot/cute_types/chat_boost_removed.py +9 -0
  18. telegrinder/bot/cute_types/chat_boost_updated.py +9 -0
  19. telegrinder/bot/cute_types/chat_join_request.py +59 -0
  20. telegrinder/bot/cute_types/chat_member_updated.py +158 -0
  21. telegrinder/bot/cute_types/chosen_inline_result.py +11 -0
  22. telegrinder/bot/cute_types/inline_query.py +41 -0
  23. telegrinder/bot/cute_types/message.py +2809 -0
  24. telegrinder/bot/cute_types/message_reaction_count_updated.py +9 -0
  25. telegrinder/bot/cute_types/message_reaction_updated.py +9 -0
  26. telegrinder/bot/cute_types/paid_media_purchased.py +11 -0
  27. telegrinder/bot/cute_types/poll.py +9 -0
  28. telegrinder/bot/cute_types/poll_answer.py +9 -0
  29. telegrinder/bot/cute_types/pre_checkout_query.py +36 -0
  30. telegrinder/bot/cute_types/shipping_query.py +11 -0
  31. telegrinder/bot/cute_types/update.py +209 -0
  32. telegrinder/bot/cute_types/utils.py +141 -0
  33. telegrinder/bot/dispatch/__init__.py +99 -0
  34. telegrinder/bot/dispatch/abc.py +74 -0
  35. telegrinder/bot/dispatch/action.py +99 -0
  36. telegrinder/bot/dispatch/context.py +162 -0
  37. telegrinder/bot/dispatch/dispatch.py +362 -0
  38. telegrinder/bot/dispatch/handler/__init__.py +23 -0
  39. telegrinder/bot/dispatch/handler/abc.py +25 -0
  40. telegrinder/bot/dispatch/handler/audio_reply.py +43 -0
  41. telegrinder/bot/dispatch/handler/base.py +34 -0
  42. telegrinder/bot/dispatch/handler/document_reply.py +43 -0
  43. telegrinder/bot/dispatch/handler/func.py +73 -0
  44. telegrinder/bot/dispatch/handler/media_group_reply.py +43 -0
  45. telegrinder/bot/dispatch/handler/message_reply.py +35 -0
  46. telegrinder/bot/dispatch/handler/photo_reply.py +43 -0
  47. telegrinder/bot/dispatch/handler/sticker_reply.py +36 -0
  48. telegrinder/bot/dispatch/handler/video_reply.py +43 -0
  49. telegrinder/bot/dispatch/middleware/__init__.py +13 -0
  50. telegrinder/bot/dispatch/middleware/abc.py +112 -0
  51. telegrinder/bot/dispatch/middleware/box.py +32 -0
  52. telegrinder/bot/dispatch/middleware/filter.py +88 -0
  53. telegrinder/bot/dispatch/middleware/media_group.py +69 -0
  54. telegrinder/bot/dispatch/process.py +93 -0
  55. telegrinder/bot/dispatch/return_manager/__init__.py +21 -0
  56. telegrinder/bot/dispatch/return_manager/abc.py +107 -0
  57. telegrinder/bot/dispatch/return_manager/callback_query.py +19 -0
  58. telegrinder/bot/dispatch/return_manager/inline_query.py +14 -0
  59. telegrinder/bot/dispatch/return_manager/message.py +34 -0
  60. telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +19 -0
  61. telegrinder/bot/dispatch/return_manager/utils.py +20 -0
  62. telegrinder/bot/dispatch/router/__init__.py +4 -0
  63. telegrinder/bot/dispatch/router/abc.py +15 -0
  64. telegrinder/bot/dispatch/router/base.py +154 -0
  65. telegrinder/bot/dispatch/view/__init__.py +15 -0
  66. telegrinder/bot/dispatch/view/abc.py +15 -0
  67. telegrinder/bot/dispatch/view/base.py +226 -0
  68. telegrinder/bot/dispatch/view/box.py +207 -0
  69. telegrinder/bot/dispatch/view/media_group.py +25 -0
  70. telegrinder/bot/dispatch/waiter_machine/__init__.py +25 -0
  71. telegrinder/bot/dispatch/waiter_machine/actions.py +16 -0
  72. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +13 -0
  73. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +53 -0
  74. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +61 -0
  75. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +49 -0
  76. telegrinder/bot/dispatch/waiter_machine/machine.py +264 -0
  77. telegrinder/bot/dispatch/waiter_machine/middleware.py +77 -0
  78. telegrinder/bot/dispatch/waiter_machine/short_state.py +105 -0
  79. telegrinder/bot/polling/__init__.py +4 -0
  80. telegrinder/bot/polling/abc.py +25 -0
  81. telegrinder/bot/polling/error_handler.py +93 -0
  82. telegrinder/bot/polling/polling.py +167 -0
  83. telegrinder/bot/polling/utils.py +12 -0
  84. telegrinder/bot/rules/__init__.py +166 -0
  85. telegrinder/bot/rules/abc.py +150 -0
  86. telegrinder/bot/rules/button.py +20 -0
  87. telegrinder/bot/rules/callback_data.py +109 -0
  88. telegrinder/bot/rules/chat_join.py +28 -0
  89. telegrinder/bot/rules/chat_member_updated.py +145 -0
  90. telegrinder/bot/rules/command.py +137 -0
  91. telegrinder/bot/rules/enum_text.py +29 -0
  92. telegrinder/bot/rules/func.py +21 -0
  93. telegrinder/bot/rules/fuzzy.py +21 -0
  94. telegrinder/bot/rules/inline.py +45 -0
  95. telegrinder/bot/rules/integer.py +19 -0
  96. telegrinder/bot/rules/is_from.py +213 -0
  97. telegrinder/bot/rules/logic.py +22 -0
  98. telegrinder/bot/rules/magic.py +60 -0
  99. telegrinder/bot/rules/markup.py +51 -0
  100. telegrinder/bot/rules/media.py +13 -0
  101. telegrinder/bot/rules/mention.py +15 -0
  102. telegrinder/bot/rules/message_entities.py +37 -0
  103. telegrinder/bot/rules/node.py +43 -0
  104. telegrinder/bot/rules/payload.py +89 -0
  105. telegrinder/bot/rules/payment_invoice.py +14 -0
  106. telegrinder/bot/rules/regex.py +34 -0
  107. telegrinder/bot/rules/rule_enum.py +71 -0
  108. telegrinder/bot/rules/start.py +73 -0
  109. telegrinder/bot/rules/state.py +35 -0
  110. telegrinder/bot/rules/text.py +27 -0
  111. telegrinder/bot/rules/update.py +14 -0
  112. telegrinder/bot/scenario/__init__.py +5 -0
  113. telegrinder/bot/scenario/abc.py +16 -0
  114. telegrinder/bot/scenario/checkbox.py +183 -0
  115. telegrinder/bot/scenario/choice.py +44 -0
  116. telegrinder/client/__init__.py +11 -0
  117. telegrinder/client/abc.py +136 -0
  118. telegrinder/client/form_data.py +34 -0
  119. telegrinder/client/rnet.py +198 -0
  120. telegrinder/model.py +133 -0
  121. telegrinder/model.pyi +57 -0
  122. telegrinder/modules.py +1081 -0
  123. telegrinder/msgspec_utils/__init__.py +42 -0
  124. telegrinder/msgspec_utils/abc.py +16 -0
  125. telegrinder/msgspec_utils/custom_types/__init__.py +6 -0
  126. telegrinder/msgspec_utils/custom_types/datetime.py +24 -0
  127. telegrinder/msgspec_utils/custom_types/enum_meta.py +61 -0
  128. telegrinder/msgspec_utils/custom_types/literal.py +25 -0
  129. telegrinder/msgspec_utils/custom_types/option.py +17 -0
  130. telegrinder/msgspec_utils/decoder.py +388 -0
  131. telegrinder/msgspec_utils/encoder.py +204 -0
  132. telegrinder/msgspec_utils/json.py +15 -0
  133. telegrinder/msgspec_utils/tools.py +80 -0
  134. telegrinder/node/__init__.py +80 -0
  135. telegrinder/node/compose.py +193 -0
  136. telegrinder/node/nodes/__init__.py +96 -0
  137. telegrinder/node/nodes/attachment.py +169 -0
  138. telegrinder/node/nodes/callback_query.py +25 -0
  139. telegrinder/node/nodes/channel.py +97 -0
  140. telegrinder/node/nodes/command.py +33 -0
  141. telegrinder/node/nodes/error.py +43 -0
  142. telegrinder/node/nodes/event.py +70 -0
  143. telegrinder/node/nodes/file.py +39 -0
  144. telegrinder/node/nodes/global_node.py +66 -0
  145. telegrinder/node/nodes/i18n.py +110 -0
  146. telegrinder/node/nodes/me.py +26 -0
  147. telegrinder/node/nodes/message_entities.py +15 -0
  148. telegrinder/node/nodes/payload.py +84 -0
  149. telegrinder/node/nodes/reply_message.py +14 -0
  150. telegrinder/node/nodes/source.py +172 -0
  151. telegrinder/node/nodes/state_mutator.py +71 -0
  152. telegrinder/node/nodes/text.py +62 -0
  153. telegrinder/node/scope.py +88 -0
  154. telegrinder/node/utils.py +38 -0
  155. telegrinder/py.typed +0 -0
  156. telegrinder/rules.py +1 -0
  157. telegrinder/tools/__init__.py +183 -0
  158. telegrinder/tools/aio.py +147 -0
  159. telegrinder/tools/final.py +21 -0
  160. telegrinder/tools/formatting/__init__.py +85 -0
  161. telegrinder/tools/formatting/deep_links/__init__.py +39 -0
  162. telegrinder/tools/formatting/deep_links/links.py +468 -0
  163. telegrinder/tools/formatting/deep_links/parsing.py +88 -0
  164. telegrinder/tools/formatting/deep_links/validators.py +8 -0
  165. telegrinder/tools/formatting/html.py +241 -0
  166. telegrinder/tools/fullname.py +82 -0
  167. telegrinder/tools/global_context/__init__.py +13 -0
  168. telegrinder/tools/global_context/abc.py +63 -0
  169. telegrinder/tools/global_context/builtin_context.py +45 -0
  170. telegrinder/tools/global_context/global_context.py +614 -0
  171. telegrinder/tools/input_file_directory.py +30 -0
  172. telegrinder/tools/keyboard/__init__.py +6 -0
  173. telegrinder/tools/keyboard/abc.py +84 -0
  174. telegrinder/tools/keyboard/base.py +108 -0
  175. telegrinder/tools/keyboard/button.py +181 -0
  176. telegrinder/tools/keyboard/data.py +31 -0
  177. telegrinder/tools/keyboard/keyboard.py +160 -0
  178. telegrinder/tools/keyboard/utils.py +95 -0
  179. telegrinder/tools/lifespan.py +188 -0
  180. telegrinder/tools/limited_dict.py +35 -0
  181. telegrinder/tools/loop_wrapper.py +271 -0
  182. telegrinder/tools/magic/__init__.py +29 -0
  183. telegrinder/tools/magic/annotations.py +172 -0
  184. telegrinder/tools/magic/descriptors.py +57 -0
  185. telegrinder/tools/magic/function.py +254 -0
  186. telegrinder/tools/magic/inspect.py +16 -0
  187. telegrinder/tools/magic/shortcut.py +107 -0
  188. telegrinder/tools/member_descriptor_proxy.py +95 -0
  189. telegrinder/tools/parse_mode.py +12 -0
  190. telegrinder/tools/serialization/__init__.py +5 -0
  191. telegrinder/tools/serialization/abc.py +34 -0
  192. telegrinder/tools/serialization/json_ser.py +60 -0
  193. telegrinder/tools/serialization/msgpack_ser.py +197 -0
  194. telegrinder/tools/serialization/utils.py +18 -0
  195. telegrinder/tools/singleton/__init__.py +4 -0
  196. telegrinder/tools/singleton/abc.py +14 -0
  197. telegrinder/tools/singleton/singleton.py +18 -0
  198. telegrinder/tools/state_mutator/__init__.py +4 -0
  199. telegrinder/tools/state_mutator/mutation.py +85 -0
  200. telegrinder/tools/state_storage/__init__.py +4 -0
  201. telegrinder/tools/state_storage/abc.py +38 -0
  202. telegrinder/tools/state_storage/memory.py +27 -0
  203. telegrinder/tools/strings.py +22 -0
  204. telegrinder/types/__init__.py +323 -0
  205. telegrinder/types/enums.py +754 -0
  206. telegrinder/types/input_file.py +51 -0
  207. telegrinder/types/methods.py +6143 -0
  208. telegrinder/types/methods_utils.py +66 -0
  209. telegrinder/types/objects.py +8184 -0
  210. telegrinder/types/webapp.py +129 -0
  211. telegrinder/verification_utils.py +35 -0
  212. telegrinder-1.0.0rc1.dist-info/METADATA +166 -0
  213. telegrinder-1.0.0rc1.dist-info/RECORD +215 -0
  214. telegrinder-1.0.0rc1.dist-info/WHEEL +4 -0
  215. telegrinder-1.0.0rc1.dist-info/licenses/LICENSE +22 -0
@@ -0,0 +1,188 @@
1
+ import asyncio
2
+ import dataclasses
3
+ import datetime
4
+ import typing
5
+ from contextlib import asynccontextmanager
6
+
7
+ from telegrinder.modules import logger
8
+ from telegrinder.tools.aio import run_task
9
+ from telegrinder.tools.fullname import fullname
10
+
11
+ type CoroutineTask[T] = typing.Coroutine[typing.Any, typing.Any, T]
12
+ type CoroutineFunc[**P, T] = typing.Callable[P, CoroutineTask[T]]
13
+ type Task[**P, T] = CoroutineFunc[P, T] | CoroutineTask[T] | DelayedTask[P]
14
+
15
+
16
+ def to_coroutine_task[T](task: Task[..., T], /) -> CoroutineTask[T]:
17
+ if asyncio.iscoroutinefunction(task) or isinstance(task, DelayedTask):
18
+ task = task()
19
+ elif not asyncio.iscoroutine(task):
20
+ raise TypeError("Task should be coroutine or coroutine function.")
21
+ return task
22
+
23
+
24
+ @dataclasses.dataclass
25
+ class DelayedTask[**P]:
26
+ _cancelled: bool = dataclasses.field(default=False, init=False, repr=False)
27
+ _event: asyncio.Event = dataclasses.field(default_factory=asyncio.Event, init=False, repr=False)
28
+ _timer: asyncio.TimerHandle | None = dataclasses.field(default=None, init=False, repr=False)
29
+
30
+ function: typing.Callable[P, CoroutineTask[typing.Any]]
31
+ seconds: float | datetime.timedelta
32
+ repeat: bool = dataclasses.field(default=False, kw_only=True)
33
+
34
+ def __post_init__(self) -> None:
35
+ self.function.cancel = self.cancel
36
+
37
+ async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> None:
38
+ stopped = False
39
+
40
+ while not stopped and not self.is_cancelled:
41
+ self.start_timer()
42
+ await self._event.wait()
43
+
44
+ try:
45
+ await self.function(*args, **kwargs)
46
+ except Exception:
47
+ await logger.aexception(
48
+ "Delayed task `{}` failed with exception, traceback message below:",
49
+ fullname(self.function),
50
+ )
51
+ finally:
52
+ self._event.clear()
53
+ self._timer = None
54
+
55
+ if not self.repeat:
56
+ stopped = True
57
+
58
+ @property
59
+ def is_cancelled(self) -> bool:
60
+ return self._cancelled
61
+
62
+ @property
63
+ def delay(self) -> float:
64
+ return float(self.seconds) if isinstance(self.seconds, int | float) else self.seconds.total_seconds()
65
+
66
+ def start_timer(self) -> None:
67
+ if self._timer is None:
68
+ self._timer = asyncio.get_running_loop().call_later(
69
+ self.delay,
70
+ callback=lambda: (self._event.set() if not self._event.is_set() else None),
71
+ )
72
+
73
+ def cancel(self) -> bool:
74
+ if self._cancelled:
75
+ return True
76
+
77
+ self._cancelled = True
78
+
79
+ if self._timer is None or self._timer.cancelled():
80
+ self._timer = None
81
+ return False
82
+
83
+ self._timer.cancel()
84
+ self._timer = None
85
+
86
+ for future in self._event._waiters:
87
+ if not future.cancelled():
88
+ future.cancel()
89
+
90
+ return True
91
+
92
+
93
+ @dataclasses.dataclass(kw_only=True, slots=True, repr=False)
94
+ class Lifespan:
95
+ _started: bool = dataclasses.field(default=False, init=False)
96
+ _lifespan_context: typing.AsyncContextManager[typing.Any] | None = dataclasses.field(default=None, init=False)
97
+ lifespan_function: typing.Callable[[], typing.AsyncContextManager[typing.Any]] | None = dataclasses.field(
98
+ default=None
99
+ )
100
+ startup_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(default_factory=lambda: [])
101
+ shutdown_tasks: list[CoroutineTask[typing.Any]] = dataclasses.field(default_factory=lambda: [])
102
+
103
+ def __repr__(self) -> str:
104
+ return "<{}: started={}>".format(fullname(self), self._started)
105
+
106
+ def __add__(self, other: object, /) -> typing.Self:
107
+ if not isinstance(other, self.__class__):
108
+ return NotImplemented
109
+
110
+ return self.__class__(
111
+ startup_tasks=self.startup_tasks + other.startup_tasks,
112
+ shutdown_tasks=self.shutdown_tasks + other.shutdown_tasks,
113
+ )
114
+
115
+ def __iadd__(self, other: object, /) -> typing.Self:
116
+ if not isinstance(other, self.__class__):
117
+ return NotImplemented
118
+
119
+ self.startup_tasks.extend(other.startup_tasks)
120
+ self.shutdown_tasks.extend(other.shutdown_tasks)
121
+ return self
122
+
123
+ async def __aenter__(self) -> None:
124
+ await self._start()
125
+
126
+ async def __aexit__(
127
+ self,
128
+ exc_type: typing.Any | None,
129
+ exc_value: typing.Any | None,
130
+ exc_tb: typing.Any | None,
131
+ ) -> None:
132
+ await self._shutdown(exc_type, exc_value, exc_tb)
133
+
134
+ def __call__[Function: typing.Callable[[], typing.AsyncGenerator[typing.Any, None]]](
135
+ self,
136
+ func: Function,
137
+ /,
138
+ ) -> Function:
139
+ self.lifespan_function = asynccontextmanager(func)
140
+ return func
141
+
142
+ @property
143
+ def started(self) -> bool:
144
+ return self._started
145
+
146
+ @staticmethod
147
+ async def _run_tasks(tasks: list[CoroutineTask[typing.Any]], /) -> None:
148
+ while tasks:
149
+ await tasks.pop(0)
150
+
151
+ async def _start(self) -> None:
152
+ if not self._started:
153
+ await logger.adebug("Running lifespan startup tasks")
154
+ self._started = True
155
+
156
+ if self.lifespan_function is not None:
157
+ self._lifespan_context = self.lifespan_function()
158
+ await self._lifespan_context.__aenter__()
159
+
160
+ await self._run_tasks(self.startup_tasks)
161
+
162
+ async def _shutdown(self, *suppress_args: typing.Any) -> None:
163
+ if self._started:
164
+ await logger.adebug("Running lifespan shutdown tasks")
165
+ self._started = False
166
+
167
+ if self._lifespan_context is not None:
168
+ await self._lifespan_context.__aexit__(*suppress_args)
169
+ self._lifespan_context = None
170
+
171
+ await self._run_tasks(self.shutdown_tasks)
172
+
173
+ def start(self, loop: asyncio.AbstractEventLoop | None = None) -> None:
174
+ run_task(self._start(), loop=loop)
175
+
176
+ def shutdown(self, loop: asyncio.AbstractEventLoop | None = None) -> None:
177
+ run_task(self._shutdown(None, None, None), loop=loop)
178
+
179
+ def on_startup[**P, T](self, task: Task[P, T], /) -> Task[P, T]:
180
+ self.startup_tasks.append(to_coroutine_task(task))
181
+ return task
182
+
183
+ def on_shutdown[**P, T](self, task: Task[P, T], /) -> Task[P, T]:
184
+ self.shutdown_tasks.append(to_coroutine_task(task))
185
+ return task
186
+
187
+
188
+ __all__ = ("CoroutineTask", "DelayedTask", "Lifespan", "run_task", "to_coroutine_task")
@@ -0,0 +1,35 @@
1
+ from collections import UserDict, deque
2
+
3
+
4
+ class LimitedDict[Key, Value](UserDict[Key, Value]):
5
+ def __init__(self, *, maxlimit: int = 1000) -> None:
6
+ super().__init__()
7
+ self.maxlimit = maxlimit
8
+ self.queue: deque[Key] = deque(maxlen=maxlimit)
9
+
10
+ def set(self, key: Key, value: Value, /) -> Value | None:
11
+ """Set item in the dictionary.
12
+ Returns a value that was deleted when the limit in the dictionary
13
+ was reached, otherwise None.
14
+ """
15
+ deleted_item = None
16
+
17
+ if len(self.queue) >= self.maxlimit:
18
+ deleted_item = self.pop(self.queue.popleft(), None)
19
+
20
+ if key not in self.queue:
21
+ self.queue.append(key)
22
+
23
+ super().__setitem__(key, value)
24
+ return deleted_item
25
+
26
+ def __setitem__(self, key: Key, value: Value, /) -> None:
27
+ self.set(key, value)
28
+
29
+ def __delitem__(self, key: Key, /) -> None:
30
+ if key in self.queue:
31
+ self.queue.remove(key)
32
+ return super().__delitem__(key)
33
+
34
+
35
+ __all__ = ("LimitedDict",)
@@ -0,0 +1,271 @@
1
+ import asyncio
2
+ import contextlib
3
+ import datetime
4
+ import enum
5
+ import signal
6
+ import typing
7
+ from contextlib import suppress
8
+
9
+ from telegrinder.modules import logger
10
+ from telegrinder.tools.aio import TaskGroup, cancel_future, loop_is_running, run_task
11
+ from telegrinder.tools.final import Final
12
+ from telegrinder.tools.fullname import fullname
13
+ from telegrinder.tools.lifespan import (
14
+ CoroutineFunc,
15
+ CoroutineTask,
16
+ DelayedTask,
17
+ Lifespan,
18
+ Task,
19
+ to_coroutine_task,
20
+ )
21
+ from telegrinder.tools.singleton.singleton import Singleton
22
+
23
+ if typing.TYPE_CHECKING:
24
+ from contextvars import Context
25
+
26
+ type Tasks = set[asyncio.Task[typing.Any]]
27
+ type DelayedFunctionDecorator[**P, R] = typing.Callable[[typing.Callable[P, R]], DelayedFunction[P, R]]
28
+
29
+ class DelayedFunction[**P, R](typing.Protocol):
30
+ __name__: str
31
+ __delayed_task__: DelayedTask[typing.Callable[P, CoroutineTask[R]]]
32
+
33
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> CoroutineTask[R]: ...
34
+
35
+ def cancel(self) -> bool:
36
+ """Cancel delayed task."""
37
+ ...
38
+
39
+
40
+ class Timer(datetime.timedelta):
41
+ repeat = False
42
+
43
+ def __call__[**P, R](self, function: CoroutineFunc[P, R], /) -> DelayedFunction[P, R]:
44
+ loop_wrapper = LoopWrapper()
45
+ loop_wrapper.add_task(DelayedTask(function, seconds=self.total_seconds(), repeat=self.repeat))
46
+ return function # type: ignore
47
+
48
+
49
+ class Interval(Timer):
50
+ repeat = True
51
+
52
+
53
+ @enum.unique
54
+ class LoopWrapperState(enum.Enum):
55
+ NOT_RUNNING = enum.auto()
56
+ RUNNING = enum.auto()
57
+ RUNNING_MANUALLY = enum.auto()
58
+ SHUTDOWN = enum.auto()
59
+
60
+
61
+ @typing.final
62
+ class LoopWrapper(Singleton, Final):
63
+ _loop: asyncio.AbstractEventLoop
64
+ _lifespan: Lifespan
65
+ _future_tasks: list[CoroutineTask[typing.Any]]
66
+ _state: LoopWrapperState
67
+ _all_tasks: set[asyncio.Task[typing.Any]]
68
+ _event_stop: asyncio.Event
69
+ _run_lw_task: asyncio.Handle
70
+ _is_attached_to_running_loop: bool
71
+
72
+ timer = Timer
73
+ interval = Interval
74
+
75
+ __slots__ = (
76
+ "_lifespan",
77
+ "_future_tasks",
78
+ "_state",
79
+ "_all_tasks",
80
+ "_loop",
81
+ "_event_stop",
82
+ "_run_lw_task",
83
+ "_is_attached_to_running_loop",
84
+ )
85
+
86
+ def __init__(self) -> None:
87
+ try:
88
+ self._loop = asyncio.get_event_loop()
89
+ except RuntimeError:
90
+ self._loop = asyncio.new_event_loop()
91
+
92
+ self._run_lw_task = self._run_lw_later()
93
+ self._lifespan = Lifespan()
94
+ self._event_stop = asyncio.Event()
95
+ self._state = LoopWrapperState.NOT_RUNNING
96
+ self._future_tasks = [self._waiter_stop()]
97
+ self._all_tasks = set()
98
+ self._is_attached_to_running_loop = False
99
+
100
+ signal.signal(signal.SIGTERM, lambda *_: self.stop())
101
+
102
+ def __repr__(self) -> str:
103
+ return "<{}: loop={!r}, lifespan={!r}>".format(
104
+ fullname(self) + (" (running)" if self.running else ""),
105
+ self._loop,
106
+ self._lifespan,
107
+ )
108
+
109
+ def __call__(self) -> asyncio.AbstractEventLoop:
110
+ return self._loop
111
+
112
+ @property
113
+ def lifespan(self) -> Lifespan:
114
+ return self._lifespan
115
+
116
+ @property
117
+ def loop(self) -> asyncio.AbstractEventLoop:
118
+ return self._loop
119
+
120
+ @property
121
+ def time(self) -> float:
122
+ return self._loop.time()
123
+
124
+ @property
125
+ def running(self) -> bool:
126
+ return self._state in {LoopWrapperState.RUNNING, LoopWrapperState.RUNNING_MANUALLY}
127
+
128
+ @property
129
+ def shutdown(self) -> bool:
130
+ return self._state is LoopWrapperState.SHUTDOWN
131
+
132
+ async def _run_async_event_loop(self) -> None:
133
+ if not self.running:
134
+ self._state = LoopWrapperState.RUNNING
135
+ async with self._async_wrap_loop():
136
+ await self._run()
137
+
138
+ async def _run(self) -> None:
139
+ await logger.adebug("Running loop wrapper")
140
+
141
+ while self._future_tasks:
142
+ self._create_task(self._future_tasks.pop(0))
143
+
144
+ while self.running and (tasks := self._get_all_tasks()):
145
+ tasks_results, _ = await asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
146
+ for task_result in tasks_results:
147
+ try:
148
+ task_result.result()
149
+ except Exception:
150
+ await logger.aexception("Traceback message below:")
151
+
152
+ async def _cancel_tasks(self) -> None:
153
+ with suppress(asyncio.exceptions.CancelledError):
154
+ await cancel_future(asyncio.gather(*self._get_all_tasks(), return_exceptions=True))
155
+
156
+ @contextlib.asynccontextmanager
157
+ async def _async_wrap_loop(self) -> typing.AsyncGenerator[typing.Any, None]:
158
+ try:
159
+ try:
160
+ await self._lifespan._start()
161
+ finally:
162
+ yield
163
+ except asyncio.CancelledError:
164
+ await logger.adebug("Cancelling tasks...")
165
+ await self._cancel_tasks()
166
+ finally:
167
+ await self._shutdown()
168
+
169
+ async def _shutdown(self) -> None:
170
+ await self.lifespan._shutdown(None, None, None)
171
+ await logger.adebug("Shutting down loop wrapper")
172
+ self._state = LoopWrapperState.SHUTDOWN
173
+
174
+ async def _waiter_stop(self) -> None:
175
+ await self._event_stop.wait()
176
+ self._state = LoopWrapperState.SHUTDOWN
177
+ self._event_stop.clear()
178
+ await self._shutdown()
179
+ await self._cancel_tasks()
180
+
181
+ def _run_lw_later(self) -> asyncio.Handle:
182
+ return self._loop.call_soon_threadsafe(lambda l: l.create_task(self._run_async_event_loop()), self._loop)
183
+
184
+ def _create_task(
185
+ self,
186
+ coro: CoroutineTask[typing.Any],
187
+ /,
188
+ name: str | None = None,
189
+ context: Context | None = None,
190
+ ) -> asyncio.Task[typing.Any]:
191
+ task = self._loop.create_task(coro, name=name, context=context)
192
+ self._all_tasks.add(task)
193
+ task.add_done_callback(self._all_tasks.discard)
194
+
195
+ try:
196
+ return task
197
+ finally:
198
+ del task
199
+
200
+ def _get_all_tasks(self) -> Tasks:
201
+ """Get a set of all tasks from the loop wrapper and event loop (`exclude the current task if any`)."""
202
+
203
+ return (self._all_tasks | asyncio.all_tasks(loop=self._loop)).symmetric_difference(
204
+ set() if (task := asyncio.current_task(self._loop)) is None else {task},
205
+ )
206
+
207
+ def _close_loop(self) -> None:
208
+ if not self._loop.is_closed():
209
+ logger.debug("Closing event loop {!r}", self._loop)
210
+ self._loop.close()
211
+
212
+ @contextlib.contextmanager
213
+ def _wrap_loop(self, *, close_loop: bool = True) -> typing.Generator[typing.Any, None, None]:
214
+ try:
215
+ try:
216
+ self.lifespan.start(self._loop)
217
+ finally:
218
+ yield
219
+ except (KeyboardInterrupt, SystemExit) as e:
220
+ print(flush=True)
221
+ logger.info(f"Caught {e.__class__.__name__}, cancelling tasks")
222
+ run_task(self._cancel_tasks(), loop=self._loop)
223
+
224
+ if isinstance(e, SystemExit):
225
+ raise
226
+ finally:
227
+ run_task(self._shutdown(), loop=self._loop)
228
+
229
+ if close_loop:
230
+ self._close_loop()
231
+
232
+ def run(self, *, close_loop: bool = True) -> typing.NoReturn: # type: ignore
233
+ if self.running:
234
+ raise RuntimeError("Loop wrapper already running.")
235
+
236
+ self._state = LoopWrapperState.RUNNING_MANUALLY
237
+ with self._wrap_loop(close_loop=close_loop):
238
+ run_task(self._run(), loop=self._loop)
239
+
240
+ def stop(self) -> None:
241
+ if not self._event_stop.is_set():
242
+ self._event_stop.set()
243
+
244
+ def add_task(self, task: Task[..., typing.Any], /) -> None:
245
+ coro_task = to_coroutine_task(task)
246
+
247
+ if not self.running:
248
+ self._future_tasks.append(coro_task)
249
+
250
+ if self._is_attached_to_running_loop is False and loop_is_running():
251
+ self.attach_to_running_loop()
252
+ else:
253
+ self._create_task(coro_task)
254
+
255
+ def attach_to_running_loop(self) -> None:
256
+ if self.running:
257
+ raise RuntimeError("Cannot attach the running loop wrapper to the running loop.")
258
+
259
+ self._loop = asyncio.get_running_loop()
260
+ self._run_lw_task.cancel()
261
+ self._is_attached_to_running_loop = True
262
+ self._run_lw_task = self._run_lw_later()
263
+
264
+ def create_task_group[T](self) -> TaskGroup[typing.Any]:
265
+ loop = asyncio.get_running_loop()
266
+ if not self.running and self._loop is not loop:
267
+ self.attach_to_running_loop()
268
+ return TaskGroup(loop)
269
+
270
+
271
+ __all__ = ("DelayedTask", "LoopWrapper", "to_coroutine_task")
@@ -0,0 +1,29 @@
1
+ from telegrinder.tools.magic.annotations import Annotations, get_generic_parameters
2
+ from telegrinder.tools.magic.descriptors import additional_property
3
+ from telegrinder.tools.magic.function import (
4
+ Bundle,
5
+ bundle,
6
+ get_default_args,
7
+ get_func_annotations,
8
+ get_func_parameters,
9
+ resolve_arg_names,
10
+ resolve_kwonly_arg_names,
11
+ resolve_posonly_arg_names,
12
+ )
13
+ from telegrinder.tools.magic.shortcut import Shortcut, shortcut
14
+
15
+ __all__ = (
16
+ "Annotations",
17
+ "Bundle",
18
+ "Shortcut",
19
+ "additional_property",
20
+ "bundle",
21
+ "get_default_args",
22
+ "get_func_annotations",
23
+ "get_func_parameters",
24
+ "get_generic_parameters",
25
+ "resolve_arg_names",
26
+ "resolve_kwonly_arg_names",
27
+ "resolve_posonly_arg_names",
28
+ "shortcut",
29
+ )
@@ -0,0 +1,172 @@
1
+ import dataclasses
2
+ import sys
3
+ import types
4
+ import typing
5
+ from annotationlib import Format, ForwardRef, get_annotations
6
+ from functools import cached_property
7
+ from reprlib import recursive_repr
8
+
9
+ from kungfu.library.misc import from_optional
10
+ from kungfu.library.monad.option import NOTHING, Option, Some
11
+
12
+ from telegrinder.tools.global_context.global_context import GlobalContext, ctx_var
13
+
14
+ type TypeParameter = typing.Union[
15
+ typing.TypeVar,
16
+ typing.TypeVarTuple,
17
+ typing.ParamSpec,
18
+ ]
19
+ type TypeParameters = tuple[TypeParameter, ...]
20
+ type SupportsAnnotations = type[typing.Any] | types.ModuleType | typing.Callable[..., typing.Any]
21
+ type AnnotationForm = typing.Any
22
+
23
+ _UNION_TYPES: typing.Final = frozenset((typing.Union, types.UnionType))
24
+ _CACHED_ANNOTATIONS: typing.Final = GlobalContext(
25
+ "cached_annotations",
26
+ thread_safe=True,
27
+ annotations=ctx_var(default_factory=dict, const=True),
28
+ )
29
+
30
+
31
+ def _cache_annotations(obj: SupportsAnnotations, annotations: dict[str, AnnotationForm], /) -> None:
32
+ _CACHED_ANNOTATIONS.annotations[obj] = MappingAnnotations(annotations)
33
+
34
+
35
+ def _get_cached_annotations(obj: SupportsAnnotations, /) -> MappingAnnotations | None:
36
+ return _CACHED_ANNOTATIONS.annotations.get(obj)
37
+
38
+
39
+ def is_union_type(obj: typing.Any, /) -> bool:
40
+ return typing.get_origin(obj) in _UNION_TYPES
41
+
42
+
43
+ class MappingAnnotations[T = AnnotationForm](dict[str, T]):
44
+ def __init__(self, annotations: typing.Mapping[str, AnnotationForm], /) -> None:
45
+ super().__init__(annotations)
46
+ self.return_type: Option[T] = from_optional(self.pop("return", None))
47
+
48
+ @recursive_repr()
49
+ def __repr__(self) -> str:
50
+ return f"annotations={super().__repr__()}, return_type={self.return_type!r}"
51
+
52
+
53
+ @dataclasses.dataclass
54
+ class Annotations:
55
+ obj: SupportsAnnotations
56
+
57
+ @cached_property
58
+ def forward_ref_parameters(self) -> dict[str, typing.Any]:
59
+ parameters = dict[str, typing.Any](
60
+ is_argument=False,
61
+ is_class=False,
62
+ module=None,
63
+ )
64
+
65
+ if isinstance(self.obj, type):
66
+ parameters["is_class"] = True
67
+ parameters["module"] = (
68
+ sys.modules[module] if (module := getattr(self.obj, "__module__", None)) is not None else None
69
+ )
70
+ elif isinstance(self.obj, types.ModuleType):
71
+ parameters["module"] = self.obj
72
+ elif callable(self.obj):
73
+ parameters["is_argument"] = True
74
+
75
+ return parameters
76
+
77
+ @cached_property
78
+ def generic_parameters(self) -> Option[dict[TypeParameter, typing.Any]]:
79
+ return get_generic_parameters(self.obj)
80
+
81
+ @classmethod
82
+ def from_obj(cls, obj: typing.Any, /) -> typing.Self:
83
+ if not isinstance(obj, type | types.ModuleType | typing.Callable):
84
+ obj = type(obj)
85
+
86
+ return cls(obj)
87
+
88
+ @typing.overload
89
+ def get(
90
+ self,
91
+ *,
92
+ ignore_failed_evals: bool = True,
93
+ cache: bool = False,
94
+ ) -> MappingAnnotations[typing.ForwardRef | AnnotationForm]: ...
95
+
96
+ @typing.overload
97
+ def get(
98
+ self,
99
+ *,
100
+ exclude_forward_refs: typing.Literal[True],
101
+ ignore_failed_evals: bool = True,
102
+ cache: bool = False,
103
+ ) -> MappingAnnotations: ...
104
+
105
+ def get(
106
+ self,
107
+ *,
108
+ exclude_forward_refs: bool = False,
109
+ ignore_failed_evals: bool = True,
110
+ cache: bool = False,
111
+ ) -> MappingAnnotations:
112
+ if (cached_annotations := _get_cached_annotations(self.obj)) is not None:
113
+ return cached_annotations
114
+
115
+ annotations = dict[str, typing.Any]()
116
+ for name, annotation in get_annotations(
117
+ obj=self.obj,
118
+ format=Format.FORWARDREF,
119
+ eval_str=False,
120
+ ).items():
121
+ if isinstance(annotation, str):
122
+ annotation = ForwardRef(annotation, owner=self.obj, **self.forward_ref_parameters)
123
+
124
+ if not isinstance(annotation, ForwardRef):
125
+ annotations[name] = annotation
126
+ continue
127
+
128
+ try:
129
+ value = annotation.evaluate(format=Format.VALUE)
130
+ except NameError:
131
+ if not ignore_failed_evals:
132
+ raise
133
+
134
+ value = annotation
135
+
136
+ if isinstance(value, typing.ForwardRef) and exclude_forward_refs:
137
+ continue
138
+
139
+ annotations[name] = value
140
+
141
+ if cache:
142
+ _cache_annotations(self.obj, annotations)
143
+
144
+ return MappingAnnotations(annotations)
145
+
146
+
147
+ def get_generic_parameters(obj: typing.Any, /) -> Option[dict[TypeParameter, AnnotationForm]]:
148
+ origin_obj = typing.get_origin(obj)
149
+ args = typing.get_args(obj)
150
+ parameters: TypeParameters = getattr(origin_obj or obj, "__parameters__")
151
+
152
+ if not parameters:
153
+ return NOTHING
154
+
155
+ index = 0
156
+ generic_alias_args = dict[TypeParameter, typing.Any]()
157
+
158
+ for parameter in parameters:
159
+ if isinstance(parameter, typing.TypeVarTuple):
160
+ stop_index = len(args) - index
161
+ generic_alias_args[parameter] = args[index:stop_index]
162
+ index = stop_index
163
+ continue
164
+
165
+ arg = args[index] if index < len(args) else None
166
+ generic_alias_args[parameter] = arg
167
+ index += 1
168
+
169
+ return Some(generic_alias_args)
170
+
171
+
172
+ __all__ = ("Annotations", "get_generic_parameters")