Pytdbot 0.10.0.dev3__py3-none-any.whl → 0.10.0.dev5__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.
pytdbot/__init__.py CHANGED
@@ -13,7 +13,7 @@ __all__ = [
13
13
  "Client",
14
14
  ]
15
15
 
16
- __version__ = "0.10.0.dev3"
16
+ __version__ = "0.10.0.dev5"
17
17
  __copyright__ = "Copyright (c) 2022-2026 Pytdbot, AYMENJD"
18
18
  __license__ = "MIT License"
19
19
 
pytdbot/client.py CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import signal
5
+ import sys
5
6
  from collections.abc import Callable
6
7
  from importlib import import_module
7
8
  from importlib import reload as reload_module
@@ -13,7 +14,11 @@ from pathlib import Path
13
14
  from platform import python_implementation, python_version
14
15
  from threading import current_thread, main_thread
15
16
 
16
- import aio_pika
17
+ try:
18
+ import nats
19
+ except ImportError:
20
+ nats = None
21
+
17
22
  from deepdiff import DeepDiff
18
23
 
19
24
  import pytdbot
@@ -48,11 +53,11 @@ class Client(Decorators, Methods):
48
53
  api_hash (``str``, *optional*):
49
54
  Identifier hash for Telegram API access, which can be obtained at https://my.telegram.org
50
55
 
51
- rabbitmq_url (``str``, *optional*):
52
- URL for RabbitMQ server connection
56
+ nats_url (``str``, *optional*):
57
+ URL for NATS server connection
53
58
 
54
59
  instance_id (``str``, *optional*):
55
- Instance ID for RabbitMQ connections and queues. Default is ``None`` (random)
60
+ Instance ID for NATS connections and queues. Default is ``None`` (random)
56
61
 
57
62
  lib_path (``str``, *optional*):
58
63
  Path to TDLib library. Default is ``None`` (auto-detect)
@@ -115,7 +120,7 @@ class Client(Decorators, Methods):
115
120
  token: str | None = None,
116
121
  api_id: int | None = None,
117
122
  api_hash: str | None = None,
118
- rabbitmq_url: str | None = None,
123
+ nats_url: str | None = None,
119
124
  instance_id: str | None = None,
120
125
  lib_path: str | None = None,
121
126
  plugins: Plugins | None = None,
@@ -137,12 +142,11 @@ class Client(Decorators, Methods):
137
142
  td_verbosity: int = 2,
138
143
  td_log: LogStream | None = None,
139
144
  user_bot: bool = False,
140
- server_ack: bool = True,
141
145
  ) -> None:
142
146
  self.__api_id = api_id
143
147
  self.__api_hash = api_hash
144
- self.__rabbitmq_url = rabbitmq_url
145
- self._rabbitmq_instance_id = (
148
+ self.__nats_url = nats_url
149
+ self._nats_instance_id = (
146
150
  instance_id if isinstance(instance_id, str) else create_extra_id(4)
147
151
  )
148
152
  self.__token = token
@@ -170,7 +174,6 @@ class Client(Decorators, Methods):
170
174
  self.load_messages_before_reply = load_messages_before_reply
171
175
  self.queue = asyncio.Queue()
172
176
  self.user_bot = user_bot
173
- self.server_ack = server_ack
174
177
  self.my_id = (
175
178
  get_bot_id_from_token(self.__token)
176
179
  if isinstance(self.__token, str)
@@ -186,7 +189,7 @@ class Client(Decorators, Methods):
186
189
  self.me: types.User = None
187
190
  self.is_authenticated = False
188
191
  self.is_reloading_plugins = False
189
- self.is_rabbitmq = True if rabbitmq_url else False
192
+ self.is_nats = True if nats_url else False
190
193
  self.options = {}
191
194
  self.allow_outgoing_message_types: tuple = (types.MessagePaymentRefunded,)
192
195
  self.get_message_methods = {
@@ -203,7 +206,6 @@ class Client(Decorators, Methods):
203
206
  self._results: dict[str, asyncio.Future] = {}
204
207
  self._workers_tasks = None
205
208
  self.__wait_login: asyncio.Event = None
206
- self.__rabbitmq_iterator_task = None
207
209
  self.__authorization_state: str = None
208
210
  self.__cache = {"is_coro_filter": {}}
209
211
  self.__local_handlers = {
@@ -219,10 +221,11 @@ class Client(Decorators, Methods):
219
221
  self.__idle_event: asyncio.Event = None
220
222
  self.__closed_event: asyncio.Event = None
221
223
 
222
- # RabbitMQ
223
- self.__rqueues = None
224
- self.__rconnection = None
225
- self.__rchannel = None
224
+ # NATS
225
+ self.__nc = None
226
+ self.__requests_subject = f"bot.{self.my_id}.requests"
227
+ self.__updates_subject = f"bot.{self.my_id}.updates"
228
+ self.__broadcast_subject = f"bot.{self.my_id}.broadcast"
226
229
 
227
230
  self.loop = None
228
231
 
@@ -252,7 +255,7 @@ class Client(Decorators, Methods):
252
255
  ) -> pytdbot.types.ServerStats | pytdbot.types.Error:
253
256
  """Returns TDLib Server stats"""
254
257
 
255
- self._check_rabbitmq()
258
+ self._check_nats()
256
259
 
257
260
  return await self.invoke({"@type": "getServerStats"})
258
261
 
@@ -272,7 +275,7 @@ class Client(Decorators, Methods):
272
275
  Unix timestamp when the event should be sent
273
276
  """
274
277
 
275
- self._check_rabbitmq()
278
+ self._check_nats()
276
279
 
277
280
  if not isinstance(name, str):
278
281
  raise ValueError("name must be str")
@@ -300,7 +303,7 @@ class Client(Decorators, Methods):
300
303
  Event ID to cancel
301
304
  """
302
305
 
303
- self._check_rabbitmq()
306
+ self._check_nats()
304
307
 
305
308
  if not isinstance(event_id, str):
306
309
  raise ValueError("event_id must be str")
@@ -324,8 +327,8 @@ class Client(Decorators, Methods):
324
327
  self.__idle_event = asyncio.Event()
325
328
  self.__closed_event = asyncio.Event()
326
329
 
327
- if self.is_rabbitmq:
328
- await self.__start_rabbitmq()
330
+ if self.is_nats:
331
+ await self.__start_nats()
329
332
  elif not self.client_manager:
330
333
  self.__wait_login = asyncio.Event() if not self.user_bot else None
331
334
 
@@ -335,25 +338,19 @@ class Client(Decorators, Methods):
335
338
  await self.client_manager.start()
336
339
  self.is_running = True
337
340
 
338
- if isinstance(self.td_log, LogStream) and not self.is_rabbitmq:
341
+ if isinstance(self.td_log, LogStream) and not self.is_nats:
339
342
  await self.__send(
340
343
  {"@type": "setLogStream", "log_stream": obj_to_dict(self.td_log)}
341
344
  )
342
345
 
343
- if isinstance(self.workers, int):
346
+ if isinstance(self.workers, int) and not self.is_nats:
344
347
  self._workers_tasks = [
345
- self.loop.create_task(
346
- self._queue_update_worker()
347
- if not self.is_rabbitmq
348
- else self.__rabbitmq_worker()
349
- )
348
+ self.loop.create_task(self._queue_update_worker())
350
349
  for _ in range(self.workers)
351
350
  ]
352
351
  self.__is_queue_worker = True
353
352
 
354
353
  self.logger.info(f"Started with {self.workers} workers")
355
- elif self.is_rabbitmq:
356
- raise ValueError("workers must be an int when using TDLib Server")
357
354
  else:
358
355
  self.__is_queue_worker = False
359
356
  self.logger.info("Started with unlimited updates processes")
@@ -670,11 +667,27 @@ class Client(Decorators, Methods):
670
667
  }:
671
668
  await self.close()
672
669
 
670
+ if self.is_nats:
671
+ # fake closing because TDLib Server doesn't allow close() from workers
672
+ await self.process_update(
673
+ obj_to_dict(
674
+ types.UpdateAuthorizationState(
675
+ authorization_state=types.AuthorizationStateClosing()
676
+ )
677
+ )
678
+ )
679
+ await self.process_update(
680
+ obj_to_dict(
681
+ types.UpdateAuthorizationState(
682
+ authorization_state=types.AuthorizationStateClosed()
683
+ )
684
+ )
685
+ )
686
+
673
687
  await self.__closed_event.wait()
674
688
 
675
- if self.is_rabbitmq:
676
- await self.__rchannel.close()
677
- await self.__rconnection.close()
689
+ if self.is_nats:
690
+ await self.__nc.close()
678
691
 
679
692
  self.__stop_client()
680
693
 
@@ -700,25 +713,25 @@ class Client(Decorators, Methods):
700
713
  return result
701
714
 
702
715
  async def __send(self, request: dict) -> None:
703
- if self.is_rabbitmq:
704
- await self.__rchannel.default_exchange.publish(
705
- aio_pika.Message(
716
+ if self.is_nats:
717
+ await self.__on_update(
718
+ await self.__nc.request(
719
+ self.__requests_subject,
706
720
  json_dumps(request, encode=True),
707
- reply_to=self.__rqueues["responses"].name,
708
- ),
709
- routing_key=self.__rqueues["requests"].name,
721
+ timeout=None,
722
+ )
710
723
  )
711
724
  else:
712
725
  self.client_manager.send(self.client_id, request)
713
726
 
714
- def _check_rabbitmq(self):
715
- assert self.is_rabbitmq, "This method is only available for TDLib Server"
727
+ def _check_nats(self):
728
+ assert self.is_nats, "This method is only available for TDLib Server"
716
729
 
717
730
  def _check_init_args(self):
718
731
  if self.user_bot:
719
732
  return
720
733
 
721
- if not self.is_rabbitmq:
734
+ if not self.is_nats:
722
735
  if not isinstance(self.__api_id, int):
723
736
  raise TypeError("api_id must be an int")
724
737
  if not isinstance(self.__api_hash, str):
@@ -849,7 +862,7 @@ class Client(Decorators, Methods):
849
862
  if handler := self.__local_handlers.get(update.get("@type")):
850
863
  self.loop.create_task(handler(update_obj))
851
864
 
852
- if not self.is_rabbitmq and self.__is_queue_worker:
865
+ if not self.is_nats and self.__is_queue_worker:
853
866
  self.queue.put_nowait(update_obj)
854
867
  else:
855
868
  await self._handle_update(update_obj)
@@ -997,7 +1010,7 @@ class Client(Decorators, Methods):
997
1010
  `AuthorizationError`
998
1011
  """
999
1012
 
1000
- if self.is_rabbitmq:
1013
+ if self.is_nats:
1001
1014
  return
1002
1015
 
1003
1016
  if isinstance(self.__database_encryption_key, str):
@@ -1041,13 +1054,15 @@ class Client(Decorators, Methods):
1041
1054
  else:
1042
1055
  raise ValueError(f"Option {k} has unsupported type {v_type}")
1043
1056
 
1044
- await self.__send(
1045
- {
1046
- "@type": "setOption",
1047
- "name": k,
1048
- "value": data,
1049
- "@extra": {"option": k, "value": v, "id": ""},
1050
- }
1057
+ self.loop.create_task(
1058
+ self.__send(
1059
+ {
1060
+ "@type": "setOption",
1061
+ "name": k,
1062
+ "value": data,
1063
+ "@extra": {"option": k, "value": v, "id": ""},
1064
+ }
1065
+ )
1051
1066
  )
1052
1067
  self.logger.debug(f"Option {k} sent with value {v}")
1053
1068
 
@@ -1068,6 +1083,7 @@ class Client(Decorators, Methods):
1068
1083
  await self.__handle_authorization_state_wait_phone_number()
1069
1084
  elif self.authorization_state == "authorizationStateReady":
1070
1085
  self.is_authenticated = True
1086
+ self.__idle_event.clear()
1071
1087
 
1072
1088
  self.me = await self.getMe()
1073
1089
  if isinstance(self.me, types.Error):
@@ -1130,56 +1146,49 @@ class Client(Decorators, Methods):
1130
1146
  f"Option {update.name} changed to {self.options[update.name]}"
1131
1147
  )
1132
1148
 
1133
- async def __get_updates_queue(self, retries=10, delay=2):
1134
- for attempt in range(retries):
1135
- try:
1136
- return await self.__rchannel.get_queue(self.my_id + "_updates")
1137
- except aio_pika.exceptions.ChannelNotFoundEntity:
1138
- self.logger.warning(
1139
- f"Attempt {attempt + 1}: TDLib Server is not running. Retrying in {delay} seconds..."
1140
- )
1141
- await asyncio.sleep(delay)
1142
- self.logger.error(
1143
- f"Could not connect to TDLib Server after {retries} attempts."
1144
- )
1145
- raise AuthorizationError(
1146
- f"Could not connect to TDLib Server after {delay * retries} seconds timeout"
1147
- )
1149
+ async def __nc_error_handler(self, e):
1150
+ self.logger.error(f"NATS connection error: {e}")
1148
1151
 
1149
- async def __start_rabbitmq(self):
1150
- self.__rconnection = await aio_pika.connect_robust(
1151
- self.__rabbitmq_url,
1152
- client_properties={
1153
- "connection_name": f"Pytdbot instance {self._rabbitmq_instance_id}"
1154
- },
1155
- )
1156
- self.__rchannel = await self.__rconnection.channel()
1152
+ async def __nc_reconnect_handler(self):
1153
+ self.logger.info("Reconnected to NATS server")
1157
1154
 
1158
- self.logger.info("Connected to TDLib server via RabbitMQ")
1155
+ async def __nc_disconnect_handler(self):
1156
+ self.logger.info("Disconnected from NATS server")
1159
1157
 
1160
- updates_queue = await self.__get_updates_queue()
1158
+ async def __nc_closed_handler(self):
1159
+ self.logger.info("Closed connection to NATS server")
1161
1160
 
1162
- notify_queue = await self.__rchannel.declare_queue(
1163
- f"{self.my_id}_notify_{self._rabbitmq_instance_id}", exclusive=True
1164
- )
1165
- await notify_queue.bind(
1166
- await self.__rchannel.get_exchange(f"{self.my_id}_broadcast")
1167
- )
1161
+ async def __start_nats(self):
1162
+ if not nats:
1163
+ raise ImportError(
1164
+ f"nats-py is not installed, please install it with `{sys.executable} -m pip install --upgrade nats-py`"
1165
+ )
1168
1166
 
1169
- responses_queue = await self.__rchannel.declare_queue(
1170
- f"{self.my_id}_res_{self._rabbitmq_instance_id}", exclusive=True
1167
+ self.__nc = await nats.connect(
1168
+ self.__nats_url,
1169
+ name=f"Pytdbot instance {self._nats_instance_id}",
1170
+ error_cb=self.__nc_error_handler,
1171
+ reconnected_cb=self.__nc_reconnect_handler,
1172
+ closed_cb=self.__nc_closed_handler,
1173
+ disconnected_cb=self.__nc_disconnect_handler,
1174
+ max_reconnect_attempts=-1,
1171
1175
  )
1172
1176
 
1173
- self.__rqueues = {
1174
- "updates": updates_queue,
1175
- "requests": await self.__rchannel.get_queue(f"{self.my_id}_requests"),
1176
- "notify": notify_queue,
1177
- "responses": responses_queue,
1178
- }
1177
+ if self.__nc.is_connected:
1178
+ self.logger.info("Connected to TDLib server via NATS")
1179
+ else:
1180
+ raise AuthorizationError("Failed to connect to TDLib server via NATS")
1179
1181
 
1180
1182
  self.is_running = True
1181
1183
 
1182
- await self.__rqueues["responses"].consume(self.__on_update, no_ack=True)
1184
+ if not self.no_updates:
1185
+ await self.__nc.subscribe(
1186
+ self.__updates_subject,
1187
+ queue="updates",
1188
+ cb=self.__on_update,
1189
+ )
1190
+
1191
+ await self.__nc.subscribe(self.__broadcast_subject, cb=self.__on_update)
1183
1192
 
1184
1193
  await self._set_options()
1185
1194
 
@@ -1189,50 +1198,8 @@ class Client(Decorators, Methods):
1189
1198
  # since it's not part of the object
1190
1199
  await self.process_update(obj_to_dict(update))
1191
1200
 
1192
- if not self.no_updates:
1193
- self.__rabbitmq_iterator_task = self.loop.create_task(
1194
- self.__rabbitmq_iterator()
1195
- )
1196
-
1197
- await self.__rqueues["notify"].consume(self.__on_update, no_ack=True)
1198
-
1199
- async def __rabbitmq_iterator(self):
1200
- async with self.__rqueues["updates"].iterator(
1201
- no_ack=not self.server_ack
1202
- ) as iterator:
1203
- async for message in iterator:
1204
- if self.queue.qsize() > self.queue_size:
1205
- await message.nack(requeue=True)
1206
- continue
1207
-
1208
- self.queue.put_nowait(message)
1209
-
1210
- async def __rabbitmq_worker(self):
1211
- while self.is_running:
1212
- try:
1213
- message: aio_pika.IncomingMessage = self.queue.get_nowait()
1214
- except asyncio.QueueEmpty:
1215
- message: aio_pika.IncomingMessage = await self.queue.get()
1216
-
1217
- try:
1218
- update = json_loads(message.body)
1219
- if self.__is_closing and not isinstance(
1220
- update, types.UpdateAuthorizationState
1221
- ):
1222
- await message.nack(requeue=True)
1223
- continue
1224
-
1225
- await self.process_update(update)
1226
- except Exception:
1227
- self.logger.exception("Error processing message")
1228
-
1229
- await message.ack() # ack after processing
1230
-
1231
- async def __handle_rabbitmq_message(self, message: aio_pika.IncomingMessage):
1232
- await self.process_update(json_loads(message.body))
1233
-
1234
1201
  async def __on_update(self, update):
1235
- self.loop.create_task(self.__handle_rabbitmq_message(update))
1202
+ self.loop.create_task(self.process_update(json_loads(update.data)))
1236
1203
 
1237
1204
  async def __handle_update_user(self, update: types.UpdateUser):
1238
1205
  if self.is_authenticated and self.me and update.user.id == self.me.id:
@@ -1249,7 +1216,7 @@ class Client(Decorators, Methods):
1249
1216
 
1250
1217
  async def __handle_authorization_state_wait_phone_number(self):
1251
1218
  if (
1252
- self.is_rabbitmq
1219
+ self.is_nats
1253
1220
  or self.authorization_state != "authorizationStateWaitPhoneNumber"
1254
1221
  or not self.__token
1255
1222
  ):
@@ -1265,9 +1232,6 @@ class Client(Decorators, Methods):
1265
1232
  self.is_authenticated = False
1266
1233
  self.is_running = False
1267
1234
 
1268
- if self.__rabbitmq_iterator_task:
1269
- self.__rabbitmq_iterator_task.cancel()
1270
-
1271
1235
  if self.__is_queue_worker:
1272
1236
  for worker_task in self._workers_tasks:
1273
1237
  worker_task.cancel()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Pytdbot
3
- Version: 0.10.0.dev3
3
+ Version: 0.10.0.dev5
4
4
  Summary: Easy-to-use asynchronous TDLib wrapper for Python.
5
5
  Author-email: AYMEN A <let.me.code.safe@gmail.com>
6
6
  License-Expression: MIT
@@ -23,9 +23,10 @@ Requires-Python: >=3.10
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
25
  Requires-Dist: deepdiff
26
- Requires-Dist: aio_pika
27
26
  Provides-Extra: tdjson
28
27
  Requires-Dist: tdjson; extra == "tdjson"
28
+ Provides-Extra: nats
29
+ Requires-Dist: nats-py; extra == "nats"
29
30
  Dynamic: license-file
30
31
 
31
32
  # Pytdbot
@@ -60,7 +61,6 @@ Pytdbot is an asynchronous [**TDLib**](https://github.com/tdlib/td) wrapper for
60
61
  - Telegram [API key](https://my.telegram.org/apps)
61
62
  - [tdjson](https://github.com/AYMENJD/tdjson) or [TDLib](https://github.com/tdlib/td#building)
62
63
  - [deepdiff](https://github.com/seperman/deepdiff)
63
- - [aio-pika](https://github.com/mosquito/aio-pika)
64
64
 
65
65
  ### Installation
66
66
 
@@ -1,5 +1,5 @@
1
- pytdbot/__init__.py,sha256=BrOWyQWbHDBcm-01O9a1nMrbCyYDbbsOHzAkX63NV-g,424
2
- pytdbot/client.py,sha256=e4QrDWfl_8AYUC7RZyB6bB3N8jDqULIn-bCzHbXs3Vk,47647
1
+ pytdbot/__init__.py,sha256=PTdb68DV7VmMQHsCyPEbSxhsWqs7dQIXdWxdwBK5TtI,424
2
+ pytdbot/client.py,sha256=c57DSHi2oH5Fx9HurQue5V2L3AANrFrliuV48FtDi6E,45940
3
3
  pytdbot/client_manager.py,sha256=jWWosyRY57XAL8u2UmsE9ftpLHhJG8moJxeSIvVjgFs,6515
4
4
  pytdbot/filters.py,sha256=V6b09AGNVrw4ipgRMFYD9jJKAcNo9_WIJ-bSaGwSyf4,1549
5
5
  pytdbot/exception/__init__.py,sha256=K58aYatmsHdbnYuDThr9akCrCrN-_TiWbfrXpCGVSCI,540
@@ -32,8 +32,8 @@ pytdbot/utils/obj_encoder.py,sha256=VmFdYtGloV3wOQx-srdrg4HY8cIYiGmFik0VmYFZB4w,
32
32
  pytdbot/utils/strings.py,sha256=0jVBjlM0bKV5iCZy5ZipBTCPvdaqMxhlp1dxnu5W2dY,1256
33
33
  pytdbot/utils/text_format.py,sha256=g-PvtYr0dvltCa120IcEMff6LnBeI_F_6m7anYtEFaE,9762
34
34
  pytdbot/utils/webapps.py,sha256=qD8j3wpgAn2KgxEZMoh7k8FEom8JNiPN3rKweAS2lGw,2428
35
- pytdbot-0.10.0.dev3.dist-info/licenses/LICENSE,sha256=Y77J1RSAYfRz6kBjIWq81eWgLFQ_Su_b1l5rWthsHCM,1078
36
- pytdbot-0.10.0.dev3.dist-info/METADATA,sha256=ui0S-ybfGHWowtP-PgOI5SHPajwdHmzbJGMVtj47cnw,11040
37
- pytdbot-0.10.0.dev3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
38
- pytdbot-0.10.0.dev3.dist-info/top_level.txt,sha256=EkLKG-BZysNAC-td3TJQQAomlT6YsvKWwei1Pzk6Oqg,8
39
- pytdbot-0.10.0.dev3.dist-info/RECORD,,
35
+ pytdbot-0.10.0.dev5.dist-info/licenses/LICENSE,sha256=Y77J1RSAYfRz6kBjIWq81eWgLFQ_Su_b1l5rWthsHCM,1078
36
+ pytdbot-0.10.0.dev5.dist-info/METADATA,sha256=hdDDkNKNpQDPBNfAgcgYUv6f6NPfoS4nykY5UOcTywQ,11026
37
+ pytdbot-0.10.0.dev5.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
38
+ pytdbot-0.10.0.dev5.dist-info/top_level.txt,sha256=EkLKG-BZysNAC-td3TJQQAomlT6YsvKWwei1Pzk6Oqg,8
39
+ pytdbot-0.10.0.dev5.dist-info/RECORD,,