Pytdbot 0.9.4__tar.gz → 0.9.5.dev0__tar.gz

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 (46) hide show
  1. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/PKG-INFO +1 -1
  2. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/Pytdbot.egg-info/PKG-INFO +1 -1
  3. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/Pytdbot.egg-info/SOURCES.txt +3 -0
  4. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/__init__.py +1 -1
  5. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/client.py +107 -12
  6. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/handlers/decorators.py +45 -0
  7. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/types/__init__.py +4 -0
  8. pytdbot-0.9.5.dev0/pytdbot/types/tdserver/__init__.py +3 -0
  9. pytdbot-0.9.5.dev0/pytdbot/types/tdserver/schedule.py +96 -0
  10. pytdbot-0.9.5.dev0/pytdbot/types/tdserver/stats.py +65 -0
  11. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/utils/__init__.py +4 -0
  12. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/utils/text_format.py +44 -12
  13. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/LICENSE +0 -0
  14. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/MANIFEST.in +0 -0
  15. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/Pytdbot.egg-info/dependency_links.txt +0 -0
  16. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/Pytdbot.egg-info/requires.txt +0 -0
  17. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/Pytdbot.egg-info/top_level.txt +0 -0
  18. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/README.md +0 -0
  19. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/client_manager.py +0 -0
  20. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/exception/__init__.py +0 -0
  21. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/filters.py +0 -0
  22. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/handlers/__init__.py +0 -0
  23. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/handlers/handler.py +0 -0
  24. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/handlers/td_updates.py +0 -0
  25. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/methods/__init__.py +0 -0
  26. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/methods/methods.py +0 -0
  27. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/methods/td_functions.py +0 -0
  28. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/tdjson/__init__.py +0 -0
  29. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/tdjson/tdjson.py +0 -0
  30. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/types/plugins/__init__.py +0 -0
  31. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/types/td_types/__init__.py +0 -0
  32. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/types/td_types/bound_methods/__init__.py +0 -0
  33. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/types/td_types/bound_methods/callback_query.py +0 -0
  34. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/types/td_types/bound_methods/chatActions.py +0 -0
  35. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/types/td_types/bound_methods/file.py +0 -0
  36. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/types/td_types/bound_methods/message.py +0 -0
  37. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/types/td_types/types.py +0 -0
  38. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/utils/asyncio_utils.py +0 -0
  39. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/utils/escape.py +0 -0
  40. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/utils/json_utils.py +0 -0
  41. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/utils/obj_encoder.py +0 -0
  42. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/utils/strings.py +0 -0
  43. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/pytdbot/utils/webapps.py +0 -0
  44. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/requirements.txt +0 -0
  45. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/setup.cfg +0 -0
  46. {pytdbot-0.9.4 → pytdbot-0.9.5.dev0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Pytdbot
3
- Version: 0.9.4
3
+ Version: 0.9.5.dev0
4
4
  Summary: Easy-to-use asynchronous TDLib wrapper for Python.
5
5
  Home-page: https://github.com/pytdbot/client
6
6
  Author: AYMEN Mohammed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Pytdbot
3
- Version: 0.9.4
3
+ Version: 0.9.5.dev0
4
4
  Summary: Easy-to-use asynchronous TDLib wrapper for Python.
5
5
  Home-page: https://github.com/pytdbot/client
6
6
  Author: AYMEN Mohammed
@@ -31,6 +31,9 @@ pytdbot/types/td_types/bound_methods/callback_query.py
31
31
  pytdbot/types/td_types/bound_methods/chatActions.py
32
32
  pytdbot/types/td_types/bound_methods/file.py
33
33
  pytdbot/types/td_types/bound_methods/message.py
34
+ pytdbot/types/tdserver/__init__.py
35
+ pytdbot/types/tdserver/schedule.py
36
+ pytdbot/types/tdserver/stats.py
34
37
  pytdbot/utils/__init__.py
35
38
  pytdbot/utils/asyncio_utils.py
36
39
  pytdbot/utils/escape.py
@@ -13,7 +13,7 @@ __all__ = [
13
13
  "Client",
14
14
  ]
15
15
 
16
- __version__ = "0.9.4"
16
+ __version__ = "0.9.5.dev0"
17
17
  __copyright__ = "Copyright (c) 2022-2025 Pytdbot, AYMENJD"
18
18
  __license__ = "MIT License"
19
19
 
@@ -102,6 +102,9 @@ class Client(Decorators, Methods):
102
102
 
103
103
  td_log (:class:`~pytdbot.types.LogStream`, *optional*):
104
104
  Log stream. Default is ``None`` (Log to ``stdout``)
105
+
106
+ user_bot (``bool``, *optional*):
107
+ Pass ``True`` if this is a user-bot. Default is ``False``
105
108
  """
106
109
 
107
110
  def __init__(
@@ -183,6 +186,7 @@ class Client(Decorators, Methods):
183
186
  self._current_handlers = {}
184
187
  self._results: Dict[str, asyncio.Future] = {}
185
188
  self._workers_tasks = None
189
+ self.__rabbitmq_iterator_task = None
186
190
  self.__authorization_state = None
187
191
  self.__cache = {"is_coro_filter": {}}
188
192
  self.__local_handlers = {
@@ -226,17 +230,72 @@ class Client(Decorators, Methods):
226
230
 
227
231
  return self.__authorization_state
228
232
 
233
+ async def getServerStats(
234
+ self,
235
+ ) -> Union["pytdbot.types.ServerStats", "pytdbot.types.Error"]:
236
+ """Returns TDLib Server stats"""
237
+
238
+ self._check_rabbitmq()
239
+
240
+ return await self.invoke({"@type": "getServerStats"})
241
+
242
+ async def scheduleEvent(
243
+ self, data: dict, send_at: int
244
+ ) -> Union["pytdbot.types.ScheduledEvent", "pytdbot.types.Error"]:
245
+ """Schedule an event
246
+
247
+ Parameters:
248
+ data (:class:`dict`):
249
+ The event data to be scheduled
250
+
251
+ send_at (:class:`int`):
252
+ Unix timestamp when the event should be sent
253
+ """
254
+
255
+ self._check_rabbitmq()
256
+
257
+ if not isinstance(data, dict):
258
+ raise ValueError("data must be dict")
259
+ if not isinstance(send_at, (int, float)):
260
+ raise ValueError("send_at must be int")
261
+
262
+ return await self.invoke(
263
+ {"@type": "scheduleEvent", "data": data, "send_at": send_at}
264
+ )
265
+
266
+ async def cancelScheduledEvent(
267
+ self, event_id: str
268
+ ) -> Union["pytdbot.types.Ok", "pytdbot.types.Error"]:
269
+ """Cancel a scheduled event
270
+
271
+ Parameters:
272
+ event_id (:class:`str`):
273
+ Event ID to cancel
274
+ """
275
+
276
+ self._check_rabbitmq()
277
+
278
+ if not isinstance(event_id, str):
279
+ raise ValueError("event_id must be str")
280
+
281
+ return await self.invoke(
282
+ {"@type": "cancelScheduledEvent", "event_id": event_id}
283
+ )
284
+
229
285
  async def start(self) -> None:
230
286
  r"""Start pytdbot client"""
231
287
 
232
288
  if not self.is_running:
233
289
  self.logger.info("Starting pytdbot client...")
234
290
 
235
- if not self.client_manager:
291
+ if self.is_rabbitmq:
292
+ await self.__start_rabbitmq()
293
+ elif not self.client_manager:
236
294
  self.client_manager = ClientManager(
237
295
  self, self.lib_path, self.td_verbosity, loop=self.loop
238
296
  )
239
297
  await self.client_manager.start()
298
+ self.is_running = True
240
299
 
241
300
  if isinstance(self.td_log, LogStream) and not self.is_rabbitmq:
242
301
  await self.__send(
@@ -245,21 +304,22 @@ class Client(Decorators, Methods):
245
304
 
246
305
  if isinstance(self.workers, int):
247
306
  self._workers_tasks = [
248
- self.loop.create_task(self._queue_update_worker())
307
+ self.loop.create_task(
308
+ self._queue_update_worker()
309
+ if not self.is_rabbitmq
310
+ else self.__rabbitmq_worker()
311
+ )
249
312
  for _ in range(self.workers)
250
313
  ]
251
314
  self.__is_queue_worker = True
252
315
 
253
316
  self.logger.info(f"Started with {self.workers} workers")
317
+ elif self.is_rabbitmq:
318
+ raise ValueError("workers must be an int when using TDLib Server")
254
319
  else:
255
320
  self.__is_queue_worker = False
256
321
  self.logger.info("Started with unlimited updates processes")
257
322
 
258
- if self.is_rabbitmq:
259
- await self.__start_rabbitmq()
260
- else: # client_manager
261
- self.is_running = True
262
-
263
323
  self.loop.create_task(
264
324
  self.getOption("version")
265
325
  ) # Ping TDLib to start processing updates
@@ -535,7 +595,7 @@ class Client(Decorators, Methods):
535
595
 
536
596
  self.__stop_client()
537
597
 
538
- if not self.client_manager.start_clients_on_add:
598
+ if self.client_manager and not self.client_manager.start_clients_on_add:
539
599
  await self.client_manager.close()
540
600
 
541
601
  self.logger.info("Instance closed")
@@ -567,6 +627,9 @@ class Client(Decorators, Methods):
567
627
  else:
568
628
  self.client_manager.send(self.client_id, request)
569
629
 
630
+ def _check_rabbitmq(self):
631
+ assert self.is_rabbitmq, "This method is only available for TDLib Server"
632
+
570
633
  def _check_init_args(self):
571
634
  if self.user_bot:
572
635
  return
@@ -696,7 +759,7 @@ class Client(Decorators, Methods):
696
759
  if update_handler:
697
760
  self.loop.create_task(update_handler(update))
698
761
 
699
- if self.__is_queue_worker:
762
+ if not self.is_rabbitmq and self.__is_queue_worker:
700
763
  self.queue.put_nowait(update)
701
764
  else:
702
765
  await self._handle_update(update)
@@ -884,7 +947,9 @@ class Client(Decorators, Methods):
884
947
  f"{str(self.me.id) if not self.me.usernames else '@' + self.me.usernames.editable_username}"
885
948
  )
886
949
 
887
- if (
950
+ if self.authorization_state == "authorizationStateClosing":
951
+ self.__is_closing = True
952
+ elif (
888
953
  self.authorization_state == "authorizationStateClosed"
889
954
  and self.__is_closing is False
890
955
  ):
@@ -955,6 +1020,8 @@ class Client(Decorators, Methods):
955
1020
  )
956
1021
  self.__rchannel = await self.__rconnection.channel()
957
1022
 
1023
+ self.logger.info("Connected to TDLib server via RabbitMQ")
1024
+
958
1025
  updates_queue = await self.__get_updates_queue()
959
1026
 
960
1027
  notify_queue = await self.__rchannel.declare_queue(
@@ -968,7 +1035,7 @@ class Client(Decorators, Methods):
968
1035
 
969
1036
  self.__rqueues = {
970
1037
  "updates": updates_queue,
971
- "requests": await self.__rchannel.get_queue(self.my_id + "_requests"),
1038
+ "requests": await self.__rchannel.get_queue(f"{self.my_id}_requests"),
972
1039
  "notify": notify_queue,
973
1040
  "responses": responses_queue,
974
1041
  }
@@ -986,10 +1053,35 @@ class Client(Decorators, Methods):
986
1053
  await self.process_update(obj_to_dict(update))
987
1054
 
988
1055
  if not self.no_updates:
989
- await self.__rqueues["updates"].consume(self.__on_update, no_ack=True)
1056
+ self.__rabbitmq_iterator_task = self.loop.create_task(
1057
+ self.__rabbitmq_iterator()
1058
+ )
990
1059
 
991
1060
  await self.__rqueues["notify"].consume(self.__on_update, no_ack=True)
992
1061
 
1062
+ async def __rabbitmq_iterator(self):
1063
+ async with self.__rqueues["updates"].iterator() as iterator:
1064
+ async for message in iterator:
1065
+ await self.queue.put(message)
1066
+
1067
+ async def __rabbitmq_worker(self):
1068
+ while self.is_running:
1069
+ message: aio_pika.IncomingMessage = await self.queue.get()
1070
+
1071
+ try:
1072
+ update = json_loads(message.body)
1073
+ if self.__is_closing and not isinstance(
1074
+ update, types.UpdateAuthorizationState
1075
+ ):
1076
+ await message.nack(requeue=True)
1077
+ continue
1078
+
1079
+ await self.process_update(update)
1080
+ except Exception:
1081
+ self.logger.exception("Error processing message")
1082
+
1083
+ await message.ack() # ack after processing
1084
+
993
1085
  async def __handle_rabbitmq_message(self, message: aio_pika.IncomingMessage):
994
1086
  await self.process_update(json_loads(message.body))
995
1087
 
@@ -1026,6 +1118,9 @@ class Client(Decorators, Methods):
1026
1118
  self.is_authenticated = False
1027
1119
  self.is_running = False
1028
1120
 
1121
+ if self.__rabbitmq_iterator_task:
1122
+ self.__rabbitmq_iterator_task.cancel()
1123
+
1029
1124
  if self.__is_queue_worker:
1030
1125
  for worker_task in self._workers_tasks:
1031
1126
  worker_task.cancel()
@@ -134,3 +134,48 @@ class Decorators(Updates):
134
134
  return func
135
135
 
136
136
  return decorator
137
+
138
+ def on_updateScheduledEvent(
139
+ self: "pytdbot.Client" = None,
140
+ filters: "pytdbot.filters.Filter" = None,
141
+ position: int = None,
142
+ inner_object: bool = False,
143
+ ) -> None:
144
+ r"""A scheduled event has been triggered
145
+
146
+ Parameters:
147
+ filters (:class:`~pytdbot.filters.Filter`, *optional*):
148
+ An update filter
149
+
150
+ position (``int``, *optional*):
151
+ The function position in handlers list. Default is ``None`` (append)
152
+
153
+ inner_object (``bool``, *optional*):
154
+ Wether to pass an inner object of update or not; for example ``UpdateNewMessage.message``. Default is ``False``
155
+
156
+ Raises:
157
+ :py:class:`TypeError`
158
+ """
159
+
160
+ def decorator(func: Callable) -> Callable:
161
+ if hasattr(func, "_handler"):
162
+ return func
163
+ elif isinstance(self, pytdbot.Client):
164
+ if iscoroutinefunction(func):
165
+ self.add_handler(
166
+ "updateScheduledEvent", func, filters, position, inner_object
167
+ )
168
+ else:
169
+ raise TypeError("Handler must be async")
170
+ elif isinstance(self, pytdbot.filters.Filter):
171
+ func._handler = Handler(
172
+ func, "updateScheduledEvent", self, position, inner_object
173
+ )
174
+ else:
175
+ func._handler = Handler(
176
+ func, "updateScheduledEvent", filters, position, inner_object
177
+ )
178
+
179
+ return func
180
+
181
+ return decorator
@@ -1,6 +1,9 @@
1
1
  __all__ = [
2
2
  "TlObject",
3
3
  "Plugins",
4
+ "ServerStats",
5
+ "ScheduledEvent",
6
+ "UpdateScheduledEvent",
4
7
  "AuthenticationCodeType",
5
8
  "EmailAddressAuthentication",
6
9
  "EmailAddressResetState",
@@ -3901,5 +3904,6 @@ from .td_types import (
3901
3904
  Updates,
3902
3905
  )
3903
3906
  from .plugins import Plugins
3907
+ from .tdserver import ServerStats, ScheduledEvent, UpdateScheduledEvent
3904
3908
 
3905
3909
  TDLIB_VERSION = "1.8.50"
@@ -0,0 +1,3 @@
1
+ __all__ = ("ServerStats", "ScheduledEvent", "UpdateScheduledEvent")
2
+ from .schedule import ScheduledEvent, UpdateScheduledEvent
3
+ from .stats import ServerStats
@@ -0,0 +1,96 @@
1
+ import pytdbot
2
+ from typing import Literal, Union
3
+
4
+
5
+ class ScheduledEvent:
6
+ r"""Describes a scheduled event
7
+
8
+ Parameters:
9
+ event_id (:class:`str`):
10
+ Unique identifier of the scheduled event
11
+
12
+ send_at (:class:`int`):
13
+ Point in time \(Unix timestamp\) when the scheduled event will be sent
14
+ """
15
+
16
+ def __init__(
17
+ self,
18
+ event_id: str = "",
19
+ send_at: int = 0,
20
+ ) -> None:
21
+ self.event_id = event_id
22
+ r"""Unique identifier of the scheduled event"""
23
+ self.send_at = send_at
24
+ r"""Point in time \(Unix timestamp\) when the scheduled event will be sent"""
25
+
26
+ def __str__(self):
27
+ return str(pytdbot.utils.obj_to_json(self, indent=4))
28
+
29
+ def getType(self) -> Literal["scheduledEvent"]:
30
+ return "scheduledEvent"
31
+
32
+ def getClass(self) -> Literal["ScheduledEvent"]:
33
+ return "ScheduledEvent"
34
+
35
+ def to_dict(self) -> dict:
36
+ return {
37
+ "@type": self.getType(),
38
+ "event_id": self.event_id,
39
+ "send_at": self.send_at,
40
+ }
41
+
42
+ @classmethod
43
+ def from_dict(cls, data: dict) -> Union["ScheduledEvent", None]:
44
+ if data:
45
+ data_class = cls()
46
+ data_class.event_id = data.get("event_id", None)
47
+ data_class.send_at = data.get("send_at", None)
48
+
49
+ return data_class
50
+
51
+
52
+ class UpdateScheduledEvent:
53
+ r"""A scheduled event
54
+
55
+ Parameters:
56
+ event_id (:class:`str`):
57
+ Unique identifier of the scheduled event
58
+
59
+ data (:class:`dict`):
60
+ Event data
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ event_id: str = "",
66
+ data: dict = None,
67
+ ) -> None:
68
+ self.event_id = event_id
69
+ r"""Unique identifier of the scheduled event"""
70
+ self.data = data
71
+ r"""Event data"""
72
+
73
+ def __str__(self):
74
+ return str(pytdbot.utils.obj_to_json(self, indent=4))
75
+
76
+ def getType(self) -> Literal["updateScheduledEvent"]:
77
+ return "updateScheduledEvent"
78
+
79
+ def getClass(self) -> Literal["UpdateScheduledEvent"]:
80
+ return "UpdateScheduledEvent"
81
+
82
+ def to_dict(self) -> dict:
83
+ return {
84
+ "@type": self.getType(),
85
+ "event_id": self.event_id,
86
+ "data": self.data,
87
+ }
88
+
89
+ @classmethod
90
+ def from_dict(cls, data: dict) -> Union["UpdateScheduledEvent", None]:
91
+ if data:
92
+ data_class = cls()
93
+ data_class.event_id = data.get("event_id", None)
94
+ data_class.data = data.get("data", None)
95
+
96
+ return data_class
@@ -0,0 +1,65 @@
1
+ import pytdbot
2
+ from typing import Literal, Union
3
+
4
+
5
+ class ServerStats:
6
+ r"""Describes TDLib Server stats
7
+
8
+ Parameters:
9
+ my_id (:class:`int`):
10
+ Identifier of the current user
11
+
12
+ uptime (:class:`int`):
13
+ Server uptime in seconds
14
+
15
+ updates_count (:class:`int`):
16
+ Total number of received updates
17
+
18
+ requests_count (:class:`int`):
19
+ Total number of sent requests
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ my_id: int = 0,
25
+ uptime: int = 0,
26
+ updates_count: int = 0,
27
+ requests_count: int = 0,
28
+ ) -> None:
29
+ self.my_id = my_id
30
+ r"""Identifier of the current user"""
31
+ self.uptime = uptime
32
+ r"""Server uptime in seconds"""
33
+ self.updates_count = updates_count
34
+ r"""Total number of received updates"""
35
+ self.requests_count = requests_count
36
+ r"""Total number of sent requests"""
37
+
38
+ def __str__(self):
39
+ return str(pytdbot.utils.obj_to_json(self, indent=4))
40
+
41
+ def getType(self) -> Literal["serverStats"]:
42
+ return "serverStats"
43
+
44
+ def getClass(self) -> Literal["ServerStats"]:
45
+ return "ServerStats"
46
+
47
+ def to_dict(self) -> dict:
48
+ return {
49
+ "@type": self.getType(),
50
+ "my_id": self.my_id,
51
+ "uptime": self.uptime,
52
+ "updates_count": self.updates_count,
53
+ "requests_count": self.requests_count,
54
+ }
55
+
56
+ @classmethod
57
+ def from_dict(cls, data: dict) -> Union["ServerStats", None]:
58
+ if data:
59
+ data_class = cls()
60
+ data_class.my_id = data.get("my_id", None)
61
+ data_class.uptime = data.get("uptime", None)
62
+ data_class.updates_count = data.get("updates_count", None)
63
+ data_class.requests_count = data.get("requests_count", None)
64
+
65
+ return data_class
@@ -26,6 +26,8 @@ __all__ = [
26
26
  "pre",
27
27
  "pre_code",
28
28
  "quote",
29
+ "rtl",
30
+ "ltr",
29
31
  ]
30
32
 
31
33
  from .asyncio_utils import get_running_loop
@@ -52,4 +54,6 @@ from .text_format import (
52
54
  pre,
53
55
  pre_code,
54
56
  quote,
57
+ rtl,
58
+ ltr,
55
59
  )
@@ -15,7 +15,7 @@ def bold(text: str, html: bool = True, escape: bool = True) -> str:
15
15
  Whether escape special characters to the given text or not. Default is ``True``
16
16
 
17
17
  Returns:
18
- py:class:`str`: The formated text
18
+ :py:class:`str`: The formated text
19
19
  """
20
20
 
21
21
  if html:
@@ -38,7 +38,7 @@ def italic(text: str, html: bool = True, escape: bool = True) -> str:
38
38
  Whether escape special characters to the given text or not. Default is ``True``
39
39
 
40
40
  Returns:
41
- py:class:`str`: The formated text
41
+ :py:class:`str`: The formated text
42
42
  """
43
43
 
44
44
  if html:
@@ -61,7 +61,7 @@ def underline(text: str, html: bool = True, escape: bool = True) -> str:
61
61
  Whether escape special characters to the given text or not. Default is ``True``
62
62
 
63
63
  Returns:
64
- py:class:`str`: The formated text
64
+ :py:class:`str`: The formated text
65
65
  """
66
66
 
67
67
  if html:
@@ -84,7 +84,7 @@ def strikethrough(text: str, html: bool = True, escape: bool = True) -> str:
84
84
  Whether escape special characters to the given text or not. Default is ``True``
85
85
 
86
86
  Returns:
87
- py:class:`str`: The formated text
87
+ :py:class:`str`: The formated text
88
88
  """
89
89
 
90
90
  if html:
@@ -107,7 +107,7 @@ def spoiler(text: str, html: bool = True, escape: bool = True) -> str:
107
107
  Whether escape special characters to the given text or not. Default is ``True``
108
108
 
109
109
  Returns:
110
- py:class:`str`: The formated text
110
+ :py:class:`str`: The formated text
111
111
  """
112
112
 
113
113
  if html:
@@ -133,7 +133,7 @@ def hyperlink(text: str, url: str, html: bool = True, escape: bool = True) -> st
133
133
  Whether escape special characters to the given text or not. Default is ``True``
134
134
 
135
135
  Returns:
136
- py:class:`str`: The formated text
136
+ :py:class:`str`: The formated text
137
137
  """
138
138
 
139
139
  assert isinstance(url, str), "url must be str"
@@ -163,7 +163,7 @@ def mention(text: str, user_id: str, html: bool = True, escape: bool = True) ->
163
163
  Whether escape special characters to the given text or not. Default is ``True``
164
164
 
165
165
  Returns:
166
- py:class:`str`: The formated text
166
+ :py:class:`str`: The formated text
167
167
  """
168
168
 
169
169
  if html:
@@ -186,7 +186,7 @@ def custom_emoji(emoji: str, custom_emoji_id: int, html: bool = True) -> str:
186
186
  If ``True``, returns HTML format, if ``False`` returns MarkdownV2. Default is ``True``
187
187
 
188
188
  Returns:
189
- py:class:`str`: The formated text
189
+ :py:class:`str`: The formated text
190
190
  """
191
191
 
192
192
  if html:
@@ -209,7 +209,7 @@ def code(text: str, html: bool = True, escape: bool = True) -> str:
209
209
  Whether escape special characters to the given text or not. Default is ``True``
210
210
 
211
211
  Returns:
212
- py:class:`str`: The formated text
212
+ :py:class:`str`: The formated text
213
213
  """
214
214
 
215
215
  if html:
@@ -232,7 +232,7 @@ def pre(text: str, html: bool = True, escape: bool = True) -> str:
232
232
  Whether escape special characters to the given text or not. Default is ``True``
233
233
 
234
234
  Returns:
235
- py:class:`str`: The formated text
235
+ :py:class:`str`: The formated text
236
236
  """
237
237
 
238
238
  if html:
@@ -258,7 +258,7 @@ def pre_code(text: str, language: str, html: bool = True, escape: bool = True) -
258
258
  Whether escape special characters to the given text or not. Default is ``True``
259
259
 
260
260
  Returns:
261
- py:class:`str`: The formated text
261
+ :py:class:`str`: The formated text
262
262
  """
263
263
 
264
264
  assert isinstance(language, str), "text must be str"
@@ -288,10 +288,42 @@ def quote(text: str, expandable: bool = False, html: bool = True, escape: bool =
288
288
  Whether escape special characters to the given text or not. Default is ``True``
289
289
 
290
290
  Returns:
291
- py:class:`str`: The formated text
291
+ :py:class:`str`: The formated text
292
292
  """
293
293
 
294
294
  if html:
295
295
  return f"<blockquote{' expandable' if expandable else ''}>{text if escape is False else escape_html(str(text))}</blockquote>"
296
296
 
297
297
  return f"{'**' if expandable else ''}>{text if escape is False else escape_markdown(str(text))}"
298
+
299
+
300
+ rtl_mark = "\u200f"
301
+ ltr_mark = "\u200e"
302
+
303
+
304
+ def rtl(text: str) -> str:
305
+ r"""Add RTL (Right-to-Left) mark to the given text
306
+
307
+ Parameters:
308
+ text (``str``):
309
+ The text to convert
310
+
311
+ Returns:
312
+ :py:class:`str`: The formated text
313
+ """
314
+
315
+ return f"{rtl_mark}{text}"
316
+
317
+
318
+ def ltr(text: str) -> str:
319
+ r"""Add LTR (Left-to-Right) mark to the given text
320
+
321
+ Parameters:
322
+ text (``str``):
323
+ The text to convert
324
+
325
+ Returns:
326
+ :py:class:`str`: The formated text
327
+ """
328
+
329
+ return f"{ltr_mark}{text}"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes