Rubka 4.5.2__py3-none-any.whl → 4.6.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.
rubka/api.py CHANGED
@@ -13,6 +13,8 @@ import tempfile
13
13
  from tqdm import tqdm
14
14
  import os
15
15
  API_URL = "https://botapi.rubika.ir/v3"
16
+ import mimetypes
17
+ import re
16
18
  import sys
17
19
  import subprocess
18
20
  def install_package(package_name):
@@ -109,6 +111,7 @@ class Robot:
109
111
  self.Key = Key
110
112
  self.platform = platform
111
113
  self.web_hook = web_hook
114
+ self.hook = web_hook
112
115
  self._offset_id = None
113
116
  self.session = requests.Session()
114
117
  self.sessions: Dict[str, Dict[str, Any]] = {}
@@ -117,7 +120,7 @@ class Robot:
117
120
  self._inline_query_handler = None
118
121
  self._message_handlers: List[dict] = []
119
122
  self._callback_handlers = None
120
- self._callback_handlers = [] # ✅ این خط مهمه
123
+ self._callback_handlers = []
121
124
  if web_hook:
122
125
  try:
123
126
  json_url = requests.get(web_hook, timeout=self.timeout).json().get('url', web_hook)
@@ -133,7 +136,7 @@ class Robot:
133
136
  except Exception as e:
134
137
  logger.error(f"Failed to set webhook from {web_hook}: {e}")
135
138
  else:
136
- self.web_hook = None
139
+ self.web_hook = self.hook
137
140
 
138
141
 
139
142
 
@@ -202,14 +205,13 @@ class Robot:
202
205
 
203
206
  def _process_update(self, update: dict):
204
207
  import threading
205
- # هندل پیام inline (بدون تغییر)
208
+
206
209
  if update.get("type") == "ReceiveQuery":
207
210
  msg = update.get("inline_message", {})
208
211
  context = InlineMessage(bot=self, raw_data=msg)
209
212
  threading.Thread(target=self._handle_inline_query, args=(context,), daemon=True).start()
210
213
  return
211
214
 
212
- # هندل پیام جدید متنی
213
215
  if update.get("type") == "NewMessage":
214
216
  msg = update.get("new_message", {})
215
217
  try:
@@ -225,17 +227,14 @@ class Robot:
225
227
  text=msg.get("text"),
226
228
  raw_data=msg)
227
229
 
228
- # هندل callback ها (دکمه‌ها) - بدون تغییر
229
230
  if context.aux_data and self._callback_handlers:
230
231
  for handler in self._callback_handlers:
231
232
  if not handler["button_id"] or context.aux_data.button_id == handler["button_id"]:
232
233
  threading.Thread(target=handler["func"], args=(self, context), daemon=True).start()
233
234
  return
234
235
 
235
- # هندل پیام‌های متنی با حلقه روی تمام هندلرها
236
236
  if self._message_handlers:
237
237
  for handler in self._message_handlers:
238
- # بررسی شرط دستورات (commands)
239
238
  if handler["commands"]:
240
239
  if not context.text or not context.text.startswith("/"):
241
240
  continue
@@ -245,11 +244,9 @@ class Robot:
245
244
  continue
246
245
  context.args = parts[1:]
247
246
 
248
- # بررسی شرط فیلترها (filters)
249
247
  if handler["filters"] and not handler["filters"](context):
250
248
  continue
251
249
 
252
- # اگر شرایط بالا برقرار بود یا هندلر عمومی بود، اجرا کن و خارج شو
253
250
  threading.Thread(target=handler["func"], args=(self, context), daemon=True).start()
254
251
  return
255
252
 
@@ -279,12 +276,10 @@ class Robot:
279
276
  def _is_duplicate(self, message_id: str, max_age_sec: int = 300) -> bool:
280
277
  now = time.time()
281
278
 
282
- # حذف پیام‌های قدیمی‌تر از max_age_sec
283
279
  expired = [mid for mid, ts in self._processed_message_ids.items() if now - ts > max_age_sec]
284
280
  for mid in expired:
285
281
  del self._processed_message_ids[mid]
286
282
 
287
- # بررسی تکراری بودن پیام
288
283
  if message_id in self._processed_message_ids:
289
284
  return True
290
285
 
@@ -298,7 +293,6 @@ class Robot:
298
293
  def run(self):
299
294
  print("Bot started running...")
300
295
  self._processed_message_ids: Dict[str, float] = {}
301
- # self._offset_id می‌تواند قبلاً مقداردهی شده باشد
302
296
 
303
297
  while True:
304
298
  try:
@@ -309,12 +303,11 @@ class Robot:
309
303
  for item in updates:
310
304
  data = item.get("data", {})
311
305
 
312
- # بررسی زمان دریافت پیام و رد کردن پیام‌های قدیمی (بیش از 20 ثانیه)
313
306
  received_at_str = item.get("received_at")
314
307
  if received_at_str:
315
308
  received_at_ts = datetime.datetime.strptime(received_at_str, "%Y-%m-%d %H:%M:%S").timestamp()
316
309
  if time.time() - received_at_ts > 20:
317
- continue # رد کردن پیام قدیمی
310
+ continue
318
311
 
319
312
  update = None
320
313
  if "update" in data:
@@ -338,12 +331,11 @@ class Robot:
338
331
  if message_id is not None:
339
332
  message_id = str(message_id)
340
333
 
341
- if message_id and self._is_duplicate(message_id):
334
+ if message_id and self._is_duplicate(received_at_str):
342
335
  continue
343
336
 
344
337
  self._process_update(update)
345
338
 
346
- # ثبت پیام به عنوان پردازش شده
347
339
  if message_id:
348
340
  self._processed_message_ids[message_id] = time.time()
349
341
 
@@ -431,6 +423,10 @@ class Robot:
431
423
 
432
424
  return False
433
425
 
426
+ def get_url_file(self,file_id):
427
+ data = self._post("getFile", {'file_id': file_id})
428
+ return data.get("data").get("download_url")
429
+
434
430
 
435
431
  def get_all_member(
436
432
  self,
@@ -479,7 +475,6 @@ class Robot:
479
475
  "reply_to_message_id": reply_to_message_id,
480
476
  "chat_keypad_type": chat_keypad_type
481
477
  }
482
- # Remove None values
483
478
  payload = {k: v for k, v in payload.items() if v is not None}
484
479
  return self._post("sendLocation", payload)
485
480
 
@@ -499,7 +494,63 @@ class Robot:
499
494
  "last_name": last_name,
500
495
  "phone_number": phone_number
501
496
  })
497
+ def download(self,file_id: str, save_as: str = None, chunk_size: int = 1024 * 512, timeout_sec: int = 60, verbose: bool = False):
498
+ """
499
+ Download a file from server using its file_id with chunked transfer,
500
+ progress bar, file extension detection, custom filename, and timeout.
501
+
502
+ If save_as is not provided, filename will be extracted from
503
+ Content-Disposition header or Content-Type header extension.
504
+
505
+ Parameters:
506
+ file_id (str): The file ID to fetch the download URL.
507
+ save_as (str, optional): Custom filename to save. If None, automatically detected.
508
+ chunk_size (int, optional): Size of each chunk in bytes. Default 512KB.
509
+ timeout_sec (int, optional): HTTP timeout in seconds. Default 60.
510
+ verbose (bool, optional): Show progress messages. Default True.
511
+
512
+ Returns:
513
+ bool: True if success, raises exceptions otherwise.
514
+ """
515
+ try:
516
+ url = self.get_url_file(file_id)
517
+ if not url:
518
+ raise ValueError("Download URL not found in response.")
519
+ except Exception as e:
520
+ raise ValueError(f"Failed to get download URL: {e}")
502
521
 
522
+ try:
523
+ with requests.get(url, stream=True, timeout=timeout_sec) as resp:
524
+ if resp.status_code != 200:
525
+ raise requests.HTTPError(f"Failed to download file. Status code: {resp.status_code}")
526
+
527
+ if not save_as:
528
+ content_disp = resp.headers.get("Content-Disposition", "")
529
+ match = re.search(r'filename="?([^\";]+)"?', content_disp)
530
+ if match:
531
+ save_as = match.group(1)
532
+ else:
533
+ content_type = resp.headers.get("Content-Type", "").split(";")[0]
534
+ extension = mimetypes.guess_extension(content_type) or ".bin"
535
+ save_as = f"{file_id}{extension}"
536
+
537
+ total_size = int(resp.headers.get("Content-Length", 0))
538
+ progress = tqdm(total=total_size, unit="B", unit_scale=True)
539
+
540
+ with open(save_as, "wb") as f:
541
+ for chunk in resp.iter_content(chunk_size=chunk_size):
542
+ if chunk:
543
+ f.write(chunk)
544
+ progress.update(len(chunk))
545
+
546
+ progress.close()
547
+ if verbose:
548
+ print(f"File saved as: {save_as}")
549
+
550
+ return True
551
+
552
+ except Exception as e:
553
+ raise RuntimeError(f"Download failed: {e}")
503
554
  def get_chat(self, chat_id: str) -> Dict[str, Any]:
504
555
  """Get chat info."""
505
556
  return self._post("getChat", {"chat_id": chat_id})
@@ -600,7 +651,64 @@ class Robot:
600
651
  payload["reply_to_message_id"] = str(reply_to_message_id)
601
652
 
602
653
  return self._post("sendFile", payload)
603
-
654
+ def send_file(
655
+ self,
656
+ chat_id: str,
657
+ path: Optional[Union[str, Path]] = None,
658
+ file_id: Optional[str] = None,
659
+ caption: Optional[str] = None,
660
+ file_name: Optional[str] = None,
661
+ inline_keypad: Optional[Dict[str, Any]] = None,
662
+ chat_keypad: Optional[Dict[str, Any]] = None,
663
+ reply_to_message_id: Optional[str] = None,
664
+ disable_notification: bool = False,
665
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
666
+ ) -> Dict[str, Any]:
667
+ if path:
668
+ file_name = file_name or Path(path).name
669
+ upload_url = self.get_upload_url("File")
670
+ file_id = self.upload_media_file(upload_url, file_name, path)
671
+ if not file_id:
672
+ raise ValueError("Either path or file_id must be provided.")
673
+ return self._send_uploaded_file(
674
+ chat_id=chat_id,
675
+ file_id=file_id,
676
+ text=caption,
677
+ inline_keypad=inline_keypad,
678
+ chat_keypad=chat_keypad,
679
+ reply_to_message_id=reply_to_message_id,
680
+ disable_notification=disable_notification,
681
+ chat_keypad_type=chat_keypad_type
682
+ )
683
+ def re_send_file(
684
+ self,
685
+ chat_id: str,
686
+ path: Optional[Union[str, Path]] = None,
687
+ file_id: Optional[str] = None,
688
+ caption: Optional[str] = None,
689
+ file_name: Optional[str] = None,
690
+ inline_keypad: Optional[Dict[str, Any]] = None,
691
+ chat_keypad: Optional[Dict[str, Any]] = None,
692
+ reply_to_message_id: Optional[str] = None,
693
+ disable_notification: bool = False,
694
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
695
+ ) -> Dict[str, Any]:
696
+ if path:
697
+ file_name = file_name or Path(path).name
698
+ upload_url = self.get_upload_url("File")
699
+ file_id = self.upload_media_file(upload_url, file_name, path)
700
+ if not file_id:
701
+ raise ValueError("Either path or file_id must be provided.")
702
+ return self._send_uploaded_file(
703
+ chat_id=chat_id,
704
+ file_id=file_id,
705
+ text=caption,
706
+ inline_keypad=inline_keypad,
707
+ chat_keypad=chat_keypad,
708
+ reply_to_message_id=reply_to_message_id,
709
+ disable_notification=disable_notification,
710
+ chat_keypad_type=chat_keypad_type
711
+ )
604
712
  def send_document(
605
713
  self,
606
714
  chat_id: str,
@@ -612,7 +720,7 @@ class Robot:
612
720
  chat_keypad: Optional[Dict[str, Any]] = None,
613
721
  reply_to_message_id: Optional[str] = None,
614
722
  disable_notification: bool = False,
615
- chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "New"
723
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
616
724
  ) -> Dict[str, Any]:
617
725
  if path:
618
726
  file_name = file_name or Path(path).name
@@ -641,7 +749,7 @@ class Robot:
641
749
  chat_keypad: Optional[Dict[str, Any]] = None,
642
750
  reply_to_message_id: Optional[str] = None,
643
751
  disable_notification: bool = False,
644
- chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "New"
752
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
645
753
  ) -> Dict[str, Any]:
646
754
  if path:
647
755
  file_name = file_name or Path(path).name
@@ -670,7 +778,7 @@ class Robot:
670
778
  chat_keypad: Optional[Dict[str, Any]] = None,
671
779
  reply_to_message_id: Optional[str] = None,
672
780
  disable_notification: bool = False,
673
- chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "New"
781
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
674
782
  ) -> Dict[str, Any]:
675
783
  if path:
676
784
  file_name = file_name or Path(path).name
@@ -699,7 +807,7 @@ class Robot:
699
807
  chat_keypad: Optional[Dict[str, Any]] = None,
700
808
  reply_to_message_id: Optional[str] = None,
701
809
  disable_notification: bool = False,
702
- chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "New"
810
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
703
811
  ) -> Dict[str, Any]:
704
812
  if path:
705
813
  file_name = file_name or Path(path).name
@@ -728,7 +836,7 @@ class Robot:
728
836
  chat_keypad: Optional[Dict[str, Any]] = None,
729
837
  reply_to_message_id: Optional[str] = None,
730
838
  disable_notification: bool = False,
731
- chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "New"
839
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
732
840
  ) -> Dict[str, Any]:
733
841
  if path:
734
842
  file_name = file_name or Path(path).name
@@ -757,7 +865,7 @@ class Robot:
757
865
  chat_keypad: Optional[Dict[str, Any]] = None,
758
866
  reply_to_message_id: Optional[str] = None,
759
867
  disable_notification: bool = False,
760
- chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "New"
868
+ chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None"
761
869
  ) -> Dict[str, Any]:
762
870
  if path:
763
871
  file_name = file_name or Path(path).name
rubka/asynco.py CHANGED
@@ -8,8 +8,12 @@ from .logger import logger
8
8
  try:
9
9
  from .context import Message, InlineMessage
10
10
  except (ImportError, ModuleNotFoundError):
11
- # اگر به صورت مستقیم اجرا شود، از این حالت استفاده می‌کند
12
11
  from context import Message, InlineMessage
12
+
13
+ from tqdm.asyncio import tqdm
14
+ from urllib.parse import urlparse, parse_qs
15
+
16
+ import mimetypes
13
17
  from pathlib import Path
14
18
  import time
15
19
  import datetime
@@ -86,8 +90,8 @@ async def check_rubka_version():
86
90
  print("Not updating may lead to malfunctions or incompatibility.")
87
91
  print("To see new methods : @rubka_library\n\n")
88
92
 
89
- # To run the check at startup in an async context
90
- # asyncio.run(check_rubka_version())
93
+
94
+
91
95
 
92
96
  def show_last_six_words(text: str) -> str:
93
97
  """Returns the last 6 characters of a stripped string."""
@@ -222,7 +226,39 @@ class Robot:
222
226
  })
223
227
  return func
224
228
  return decorator
229
+ def on_inline_query_prefix(self, prefix: str, button_id: Optional[str] = None):
230
+ if not prefix.startswith('/'):
231
+ prefix = '/' + prefix
232
+
233
+ def decorator(func: Callable[[Any, InlineMessage], None]):
234
+
235
+ async def handler_wrapper(bot_instance, inline_message: InlineMessage):
236
+
237
+ if not inline_message.raw_data or 'text' not in inline_message.raw_data:
238
+ return
239
+
240
+ query_text = inline_message.raw_data['text']
225
241
 
242
+
243
+ if query_text.startswith(prefix):
244
+
245
+
246
+
247
+ try:
248
+ await func(bot_instance, inline_message)
249
+ except Exception as e:
250
+ print(f"Error in inline query prefix handler '{prefix}': {e}")
251
+
252
+
253
+ self._inline_query_handlers.append({
254
+ "func": handler_wrapper,
255
+ "button_id": button_id
256
+
257
+
258
+
259
+ })
260
+ return func
261
+ return decorator
226
262
  async def _process_update(self, update: dict):
227
263
  if update.get("type") == "ReceiveQuery":
228
264
  msg = update.get("inline_message", {})
@@ -245,40 +281,40 @@ class Robot:
245
281
  text=msg.get("text"),
246
282
  raw_data=msg)
247
283
 
248
- # پردازش دکمه‌های شیشه‌ای (بدون تغییر)
284
+
249
285
  if context.aux_data and self._callback_handlers:
250
286
  for handler in self._callback_handlers:
251
287
  if not handler["button_id"] or context.aux_data.button_id == handler["button_id"]:
252
288
  asyncio.create_task(handler["func"](self, context))
253
289
  return
254
290
 
255
- # پردازش پیام‌های متنی با حلقه روی تمام هندلرها
291
+
256
292
  if self._message_handlers:
257
293
  for handler_info in self._message_handlers:
258
- # بررسی شرط دستورات (commands)
294
+
259
295
  if handler_info["commands"]:
260
296
  if not context.text or not context.text.startswith("/"):
261
- continue # اگر پیام کامند نبود، این هندلر را رد کن
297
+ continue
262
298
  parts = context.text.split()
263
299
  cmd = parts[0][1:]
264
300
  if cmd not in handler_info["commands"]:
265
- continue # اگر کامند مطابقت نداشت، این هندلر را رد کن
301
+ continue
266
302
  context.args = parts[1:]
267
303
 
268
- # بررسی شرط فیلترها (filters)
304
+
269
305
  if handler_info["filters"]:
270
306
  if not handler_info["filters"](context):
271
- continue # اگر فیلتر برقرار نبود، این هندلر را رد کن
307
+ continue
308
+
272
309
 
273
- # اگر هندلری برای همه پیام‌ها باشد (بدون کامند و فیلتر)
274
310
  if not handler_info["commands"] and not handler_info["filters"]:
275
311
  asyncio.create_task(handler_info["func"](self, context))
276
- return # بعد از یافتن هندلر مناسب، از حلقه خارج شو
312
+ return
313
+
277
314
 
278
- # اگر شرایط کامند یا فیلتر برقرار بود
279
315
  if handler_info["commands"] or handler_info["filters"]:
280
316
  asyncio.create_task(handler_info["func"](self, context))
281
- return # بعد از یافتن هندلر مناسب، از حلقه خارج شو
317
+ return
282
318
 
283
319
  async def get_updates(self, offset_id: Optional[str] = None, limit: Optional[int] = None) -> Dict[str, Any]:
284
320
  data = {}
@@ -293,7 +329,7 @@ class Robot:
293
329
  if limit: params['limit'] = limit
294
330
  async with session.get(self.web_hook, params=params) as response:
295
331
  response.raise_for_status()
296
- # وب‌هوک باید لیستی از رویدادها را برگرداند
332
+
297
333
  return await response.json()
298
334
 
299
335
  def _is_duplicate(self, message_id: str, max_age_sec: int = 300) -> bool:
@@ -307,6 +343,7 @@ class Robot:
307
343
 
308
344
  self._processed_message_ids[message_id] = now
309
345
  return False
346
+
310
347
 
311
348
  async def run(self):
312
349
  """
@@ -321,8 +358,8 @@ class Robot:
321
358
  while True:
322
359
  try:
323
360
  if self.web_hook:
324
- # ----- منطق وب‌هوک (اصلاح شده) -----
325
- # آپدیت‌ها مستقیما از وب‌هوک گرفته و پردازش می‌شوند
361
+
362
+
326
363
  webhook_data = await self.update_webhook()
327
364
  if isinstance(webhook_data, list):
328
365
  for item in webhook_data:
@@ -335,7 +372,7 @@ class Robot:
335
372
  if time.time() - received_at_ts > 20:
336
373
  continue
337
374
  except (ValueError, TypeError):
338
- pass # رد شدن در صورت فرمت اشتباه زمان
375
+ pass
339
376
 
340
377
  update = None
341
378
  if "update" in data:
@@ -353,11 +390,11 @@ class Robot:
353
390
  elif "message_id" in update:
354
391
  message_id = update.get("message_id")
355
392
 
356
- if message_id and not self._is_duplicate(str(message_id)):
393
+ if message_id and not self._is_duplicate(str(received_at_str)):
357
394
  await self._process_update(update)
358
395
 
359
396
  else:
360
- # ----- منطق Polling (بدون تغییر) -----
397
+
361
398
  get_updates_response = await self.get_updates(offset_id=self._offset_id, limit=100)
362
399
  if get_updates_response and get_updates_response.get("data"):
363
400
  updates = get_updates_response["data"].get("updates", [])
@@ -375,23 +412,49 @@ class Robot:
375
412
  if message_id and not self._is_duplicate(str(message_id)):
376
413
  await self._process_update(update)
377
414
 
378
- await asyncio.sleep(0.1) # وقفه کوتاه برای جلوگیری از مصرف CPU
415
+ await asyncio.sleep(0)
379
416
  except Exception as e:
380
417
  print(f"❌ Error in run loop: {e}")
381
- await asyncio.sleep(5) # وقفه طولانی‌تر در صورت بروز خطا
418
+ await asyncio.sleep(5)
382
419
  finally:
383
420
  if self._aiohttp_session:
384
421
  await self._aiohttp_session.close()
385
422
  print("Bot stopped and session closed.")
386
423
 
387
- async def send_message(self, chat_id: str, text: str, chat_keypad: Optional[Dict[str, Any]] = None, inline_keypad: Optional[Dict[str, Any]] = None, disable_notification: bool = False, reply_to_message_id: Optional[str] = None, chat_keypad_type: Optional[Literal["New", "Removed"]] = None) -> Dict[str, Any]:
388
- payload = {"chat_id": chat_id, "text": text, "disable_notification": disable_notification}
389
- if chat_keypad: payload["chat_keypad"] = chat_keypad
390
- if inline_keypad: payload["inline_keypad"] = inline_keypad
391
- if reply_to_message_id: payload["reply_to_message_id"] = reply_to_message_id
392
- if chat_keypad_type: payload["chat_keypad_type"] = chat_keypad_type
424
+ async def send_message(
425
+ self,
426
+ chat_id: str,
427
+ text: str,
428
+ chat_keypad: Optional[Dict[str, Any]] = None,
429
+ inline_keypad: Optional[Dict[str, Any]] = None,
430
+ disable_notification: bool = False,
431
+ reply_to_message_id: Optional[str] = None,
432
+ chat_keypad_type: Optional[Literal["New", "Removed"]] = None
433
+ ) -> Dict[str, Any]:
434
+ payload = {
435
+ "chat_id": chat_id,
436
+ "text": text,
437
+ "disable_notification": disable_notification,
438
+ }
439
+
440
+ if chat_keypad:
441
+ payload["chat_keypad"] = chat_keypad
442
+ payload["chat_keypad_type"] = chat_keypad_type or "New"
443
+
444
+ if inline_keypad:
445
+ payload["inline_keypad"] = inline_keypad
446
+
447
+ if reply_to_message_id:
448
+ payload["reply_to_message_id"] = reply_to_message_id
449
+
393
450
  return await self._post("sendMessage", payload)
394
451
 
452
+
453
+
454
+ async def get_url_file(self,file_id):
455
+ data = await self._post("getFile", {'file_id': file_id})
456
+ return data.get("data").get("download_url")
457
+
395
458
  def _get_client(self) -> Client_get:
396
459
  if self.session_name:
397
460
  return Client_get(self.session_name, self.auth, self.Key, self.platform)
@@ -407,7 +470,7 @@ class Robot:
407
470
  username = chat_info.get('username')
408
471
  user_id = chat_info.get('user_id')
409
472
 
410
- # Since client methods are sync, run them in a thread pool
473
+
411
474
  if username:
412
475
  result = await asyncio.to_thread(self.get_all_member, channel_guid, search_text=username)
413
476
  members = result.get('in_chat_members', [])
@@ -418,7 +481,7 @@ class Robot:
418
481
  return False
419
482
 
420
483
  def get_all_member(self, channel_guid: str, search_text: str = None, start_id: str = None, just_get_guids: bool = False):
421
- # This is a sync method that will be called with asyncio.to_thread
484
+
422
485
  client = self._get_client()
423
486
  return client.get_all_members(channel_guid, search_text, start_id, just_get_guids)
424
487
 
@@ -454,7 +517,7 @@ class Robot:
454
517
  path = temp_file.name
455
518
  is_temp_file = True
456
519
 
457
- file_size = os.path.getsize(path) # Note: os.path.getsize is sync, but fast enough for most cases. aiofiles can be used for async alternative if needed on huge file lists.
520
+ file_size = os.path.getsize(path)
458
521
 
459
522
  progress_bar = tqdm(total=file_size, unit='B', unit_scale=True, unit_divisor=1024, desc=f'Uploading : {name}', bar_format='{l_bar}{bar:100}{r_bar}', colour='cyan', disable=not self.show_progress)
460
523
 
@@ -478,9 +541,100 @@ class Robot:
478
541
  json_data = await response.json()
479
542
  if is_temp_file:
480
543
  os.remove(path)
481
-
544
+ print(json_data)
482
545
  return json_data.get('data', {}).get('file_id')
483
546
 
547
+
548
+ def get_extension(content_type: str) -> str:
549
+ ext = mimetypes.guess_extension(content_type)
550
+ return ext if ext else ''
551
+
552
+ async def download(self, file_id: str, save_as: str = None, chunk_size: int = 1024 * 512,timeout_sec: int = 60, verbose: bool = False):
553
+ """
554
+ Download a file from server using its file_id with chunked transfer,
555
+ progress bar, file extension detection, custom filename, and timeout.
556
+
557
+ If save_as is not provided, filename will be extracted from
558
+ Content-Disposition header or Content-Type header extension.
559
+
560
+ Parameters:
561
+ file_id (str): The file ID to fetch the download URL.
562
+ save_as (str, optional): Custom filename to save. If None, automatically detected.
563
+ chunk_size (int, optional): Size of each chunk in bytes. Default 512KB.
564
+ timeout_sec (int, optional): HTTP timeout in seconds. Default 60.
565
+ verbose (bool, optional): Show progress messages. Default True.
566
+
567
+ Returns:
568
+ bool: True if success, raises exceptions otherwise.
569
+ """
570
+
571
+ try:
572
+ url = await self.get_url_file(file_id)
573
+ if not url:
574
+ raise ValueError("Download URL not found in response.")
575
+ except Exception as e:
576
+ raise ValueError(f"Failed to get download URL: {e}")
577
+
578
+ timeout = aiohttp.ClientTimeout(total=timeout_sec)
579
+
580
+ try:
581
+ async with aiohttp.ClientSession(timeout=timeout) as session:
582
+ async with session.get(url) as resp:
583
+ if resp.status != 200:
584
+ raise aiohttp.ClientResponseError(
585
+ request_info=resp.request_info,
586
+ history=resp.history,
587
+ status=resp.status,
588
+ message="Failed to download file.",
589
+ headers=resp.headers
590
+ )
591
+
592
+ if not save_as:
593
+ content_disp = resp.headers.get("Content-Disposition", "")
594
+ import re
595
+ match = re.search(r'filename="?([^\";]+)"?', content_disp)
596
+ if match:
597
+ save_as = match.group(1)
598
+ else:
599
+ content_type = resp.headers.get("Content-Type", "").split(";")[0]
600
+ extension = mimetypes.guess_extension(content_type) or ".bin"
601
+ save_as = f"{file_id}{extension}"
602
+
603
+ total_size = int(resp.headers.get("Content-Length", 0))
604
+ progress = tqdm(total=total_size, unit="B", unit_scale=True, disable=not verbose)
605
+
606
+ async with aiofiles.open(save_as, "wb") as f:
607
+ async for chunk in resp.content.iter_chunked(chunk_size):
608
+ await f.write(chunk)
609
+ progress.update(len(chunk))
610
+
611
+ progress.close()
612
+ if verbose:
613
+ print(f"✅ File saved as: {save_as}")
614
+
615
+ return True
616
+
617
+ except aiohttp.ClientError as e:
618
+ raise aiohttp.ClientError(f"HTTP error occurred: {e}")
619
+ except asyncio.TimeoutError:
620
+ raise asyncio.TimeoutError("Download timed out.")
621
+ except Exception as e:
622
+ raise Exception(f"Error downloading file: {e}")
623
+
624
+ except aiohttp.ClientError as e:
625
+ raise aiohttp.ClientError(f"HTTP error occurred: {e}")
626
+ except asyncio.TimeoutError:
627
+ raise asyncio.TimeoutError("The download operation timed out.")
628
+ except Exception as e:
629
+ raise Exception(f"An error occurred while downloading the file: {e}")
630
+
631
+ except aiohttp.ClientError as e:
632
+ raise aiohttp.ClientError(f"HTTP error occurred: {e}")
633
+ except asyncio.TimeoutError:
634
+ raise asyncio.TimeoutError("The download operation timed out.")
635
+ except Exception as e:
636
+ raise Exception(f"An error occurred while downloading the file: {e}")
637
+
484
638
  async def get_upload_url(self, media_type: Literal['File', 'Image', 'Voice', 'Music', 'Gif', 'Video']) -> str:
485
639
  allowed = ['File', 'Image', 'Voice', 'Music', 'Gif', 'Video']
486
640
  if media_type not in allowed:
@@ -504,22 +658,25 @@ class Robot:
504
658
  raise ValueError("Either path or file_id must be provided.")
505
659
  return await self._send_uploaded_file(chat_id=chat_id, file_id=file_id, text=text, inline_keypad=inline_keypad, chat_keypad=chat_keypad, reply_to_message_id=reply_to_message_id, disable_notification=disable_notification, chat_keypad_type=chat_keypad_type)
506
660
 
507
- async def send_document(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "New") -> Dict[str, Any]:
661
+ async def send_document(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None") -> Dict[str, Any]:
508
662
  return await self._send_file_generic("File", chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type)
509
-
510
- async def send_music(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "New") -> Dict[str, Any]:
663
+ async def send_file(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, caption: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None") -> Dict[str, Any]:
664
+ return await self._send_file_generic("File", chat_id, path, file_id, caption, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type)
665
+ async def re_send(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, caption: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None") -> Dict[str, Any]:
666
+ return await self._send_file_generic("File", chat_id, path, file_id, caption, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type)
667
+ async def send_music(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None") -> Dict[str, Any]:
511
668
  return await self._send_file_generic("Music", chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type)
512
669
 
513
- async def send_video(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "New") -> Dict[str, Any]:
670
+ async def send_video(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None") -> Dict[str, Any]:
514
671
  return await self._send_file_generic("Video", chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type)
515
672
 
516
- async def send_voice(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "New") -> Dict[str, Any]:
517
- return await self._send_file_generic("Voice", chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type)
673
+ async def send_voice(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None") -> Dict[str, Any]:
674
+ return await self._send_file_generic("Video", chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type)
518
675
 
519
- async def send_image(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "New") -> Dict[str, Any]:
676
+ async def send_image(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None") -> Dict[str, Any]:
520
677
  return await self._send_file_generic("Image", chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type)
521
678
 
522
- async def send_gif(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "New") -> Dict[str, Any]:
679
+ async def send_gif(self, chat_id: str, path: Optional[Union[str, Path]] = None, file_id: Optional[str] = None, text: Optional[str] = None, file_name: Optional[str] = None, inline_keypad: Optional[Dict[str, Any]] = None, chat_keypad: Optional[Dict[str, Any]] = None, reply_to_message_id: Optional[str] = None, disable_notification: bool = False, chat_keypad_type: Optional[Literal["New", "Removed", "None"]] = "None") -> Dict[str, Any]:
523
680
  return await self._send_file_generic("Gif", chat_id, path, file_id, text, file_name, inline_keypad, chat_keypad, reply_to_message_id, disable_notification, chat_keypad_type)
524
681
 
525
682
  async def forward_message(self, from_chat_id: str, message_id: str, to_chat_id: str, disable_notification: bool = False) -> Dict[str, Any]:
rubka/context.py CHANGED
@@ -14,9 +14,9 @@ class Sticker:
14
14
  self.file = File(data.get("file", {}))
15
15
 
16
16
 
17
- # =========================
18
- # Poll
19
- # =========================
17
+
18
+
19
+
20
20
  class PollStatus:
21
21
  def __init__(self, data: dict):
22
22
  self.state: str = data.get("state")
@@ -33,9 +33,9 @@ class Poll:
33
33
  self.poll_status = PollStatus(data.get("poll_status", {}))
34
34
 
35
35
 
36
- # =========================
37
- # Location & Contact & ForwardedFrom
38
- # =========================
36
+
37
+
38
+
39
39
  class Location:
40
40
  def __init__(self, data: dict):
41
41
  self.latitude: str = data.get("latitude")
@@ -67,18 +67,18 @@ class ForwardedFrom:
67
67
  self.from_sender_id: str = data.get("from_sender_id")
68
68
 
69
69
 
70
- # =========================
71
- # AuxData
72
- # =========================
70
+
71
+
72
+
73
73
  class AuxData:
74
74
  def __init__(self, data: dict):
75
75
  self.start_id: str = data.get("start_id")
76
76
  self.button_id: str = data.get("button_id")
77
77
 
78
78
 
79
- # =========================
80
- # Button Models
81
- # =========================
79
+
80
+
81
+
82
82
  class ButtonTextbox:
83
83
  def __init__(self, data: dict):
84
84
  self.type_line: str = data.get("type_line")
@@ -221,6 +221,13 @@ class Message:
221
221
  reply_to_message_id=self.message_id,
222
222
  **kwargs
223
223
  )
224
+ def answer(self, text: str, **kwargs):
225
+ return self.bot.send_message(
226
+ self.chat_id,
227
+ text,
228
+ reply_to_message_id=self.message_id,
229
+ **kwargs
230
+ )
224
231
 
225
232
  def reply_poll(self, question: str, options: List[str], **kwargs) -> Dict[str, Any]:
226
233
  return self.bot._post("sendPoll", {
@@ -242,6 +249,31 @@ class Message:
242
249
  chat_keypad_type: Optional[str] = "None",
243
250
  disable_notification: bool = False
244
251
  ):
252
+ if chat_keypad and chat_keypad_type == "none":chat_keypad_type == "New"
253
+ return self.bot.send_document(
254
+ chat_id=self.chat_id,
255
+ path=path,
256
+ file_id=file_id,
257
+ text=text,
258
+ chat_keypad=chat_keypad,
259
+ inline_keypad=inline_keypad,
260
+ chat_keypad_type=chat_keypad_type,
261
+ disable_notification=disable_notification,
262
+ reply_to_message_id=self.message_id
263
+ )
264
+ def reply_file(
265
+ self,
266
+ path: Optional[Union[str, Path]] = None,
267
+ file_id: Optional[str] = None,
268
+ text: Optional[str] = None,
269
+ chat_keypad: Optional[Dict[str, Any]] = None,
270
+ inline_keypad: Optional[Dict[str, Any]] = None,
271
+ chat_keypad_type: Optional[str] = "None",
272
+ disable_notification: bool = False
273
+ ):
274
+ if chat_keypad and chat_keypad_type == "none":
275
+ chat_keypad_type == "New"
276
+
245
277
  return self.bot.send_document(
246
278
  chat_id=self.chat_id,
247
279
  path=path,
@@ -264,6 +296,8 @@ class Message:
264
296
  chat_keypad_type: Optional[str] = "None",
265
297
  disable_notification: bool = False
266
298
  ):
299
+ if chat_keypad and chat_keypad_type == "none":
300
+ chat_keypad_type == "New"
267
301
  return self.bot.send_image(
268
302
  chat_id=self.chat_id,
269
303
  path=path,
@@ -286,6 +320,8 @@ class Message:
286
320
  chat_keypad_type: Optional[str] = "None",
287
321
  disable_notification: bool = False
288
322
  ):
323
+ if chat_keypad and chat_keypad_type == "none":
324
+ chat_keypad_type == "New"
289
325
  return self.bot.send_music(
290
326
  chat_id=self.chat_id,
291
327
  path=path,
@@ -308,6 +344,8 @@ class Message:
308
344
  chat_keypad_type: Optional[str] = "None",
309
345
  disable_notification: bool = False
310
346
  ):
347
+ if chat_keypad and chat_keypad_type == "none":
348
+ chat_keypad_type == "New"
311
349
  return self.bot.send_voice(
312
350
  chat_id=self.chat_id,
313
351
  path=path,
@@ -330,6 +368,7 @@ class Message:
330
368
  chat_keypad_type: Optional[str] = "None",
331
369
  disable_notification: bool = False
332
370
  ):
371
+ if chat_keypad and chat_keypad_type == "none":chat_keypad_type == "New"
333
372
  return self.bot.send_gif(
334
373
  chat_id=self.chat_id,
335
374
  path=path,
@@ -388,14 +427,6 @@ class Message:
388
427
  **kwargs
389
428
  })
390
429
 
391
- def reply_file(self, file_id: str, **kwargs) -> Dict[str, Any]:
392
- return self.bot._post("sendFile", {
393
- "chat_id": self.chat_id,
394
- "file_id": file_id,
395
- "reply_to_message_id": self.message_id,
396
- **kwargs
397
- })
398
-
399
430
  def edit(self, new_text: str) -> Dict[str, Any]:
400
431
  return self.bot.edit_message_text(
401
432
  chat_id=self.chat_id,
@@ -413,12 +444,25 @@ class AuxData:
413
444
  self.start_id = data.get("start_id")
414
445
  self.button_id = data.get("button_id")
415
446
 
416
- # نمونه کلاس InlineMessage با ویژگی aux_data که شامل دکمه است
447
+
417
448
  class InlineMessage:
418
449
  def __init__(self, bot, raw_data: dict):
419
450
  self.bot = bot
420
451
  self.raw_data = raw_data
421
-
452
+ self.chat_id = self.raw_data.get("chat_id")
453
+ self.time: str = self.raw_data.get("time")
454
+ self.is_edited: bool = self.raw_data.get("is_edited", False)
455
+ self.sender_type: str = self.raw_data.get("sender_type")
456
+ self.args = []
457
+ self.reply_to_message_id: Optional[str] = self.raw_data.get("reply_to_message_id")
458
+ self.forwarded_from = ForwardedFrom(self.raw_data["forwarded_from"]) if "forwarded_from" in self.raw_data else None
459
+ self.file = File(self.raw_data["file"]) if "file" in self.raw_data else None
460
+ self.sticker = Sticker(self.raw_data["sticker"]) if "sticker" in self.raw_data else None
461
+ self.contact_message = ContactMessage(self.raw_data["contact_message"]) if "contact_message" in self.raw_data else None
462
+ self.poll = Poll(self.raw_data["poll"]) if "poll" in self.raw_data else None
463
+ self.location = Location(self.raw_data["location"]) if "location" in self.raw_data else None
464
+ self.live_location = LiveLocation(self.raw_data["live_location"]) if "live_location" in self.raw_data else None
465
+ self.aux_data = AuxData(self.raw_data["aux_data"]) if "aux_data" in self.raw_data else None
422
466
  self.chat_id: str = raw_data.get("chat_id")
423
467
  self.message_id: str = raw_data.get("message_id")
424
468
  self.sender_id: str = raw_data.get("sender_id")
@@ -432,6 +476,13 @@ class InlineMessage:
432
476
  reply_to_message_id=self.message_id,
433
477
  **kwargs
434
478
  )
479
+ def answer(self, text: str, **kwargs):
480
+ return self.bot.send_message(
481
+ self.chat_id,
482
+ text,
483
+ reply_to_message_id=self.message_id,
484
+ **kwargs
485
+ )
435
486
 
436
487
  def edit(self, new_text: str):
437
488
  return self.bot.edit_message_text(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Rubka
3
- Version: 4.5.2
3
+ Version: 4.6.0
4
4
  Summary: A Python library for interacting with Rubika Bot API.
5
5
  Home-page: https://github.com/Mahdy-Ahmadi/Rubka
6
6
  Download-URL: https://github.com/Mahdy-Ahmadi/rubka/blob/main/project_library.zip
@@ -19,6 +19,11 @@ Requires-Dist: requests
19
19
  Requires-Dist: Pillow
20
20
  Requires-Dist: websocket-client
21
21
  Requires-Dist: pycryptodome
22
+ Requires-Dist: aiohttp
23
+ Requires-Dist: tqdm
24
+ Requires-Dist: mutagen
25
+ Requires-Dist: filetype
26
+ Requires-Dist: aiofiles
22
27
  Dynamic: author
23
28
  Dynamic: author-email
24
29
  Dynamic: classifier
@@ -1,9 +1,9 @@
1
1
  rubka/__init__.py,sha256=TR1DABU5Maz2eO62ZEFiwOqNU0dH6l6HZfqRUxeo4eY,194
2
- rubka/api.py,sha256=gzaH2nfZjpxlMwzNsgA4FFBR38bAfQK2-2oTPs555Xw,35160
3
- rubka/asynco.py,sha256=m3WztgmkzK6Jvrko8HxqLlIigWTbtJkdbDdQq3NtbMo,31994
2
+ rubka/api.py,sha256=-ayu17rFI35hU1O7ePxLQNTK-upj2uJxOflA0JUdWdI,39421
3
+ rubka/asynco.py,sha256=f2OH2JDdB98x7T4c6B1n-dTWpCe8R5KkkHUdwr9IP9g,37433
4
4
  rubka/button.py,sha256=4fMSZR7vUADxSmw1R3_pZ4dw5uMLZX5sOkwPPyNTBDE,8437
5
5
  rubka/config.py,sha256=Bck59xkOiqioLv0GkQ1qPGnBXVctz1hKk6LT4h2EPx0,78
6
- rubka/context.py,sha256=j1scXTy_wBY52MmMixfZyIQnB0sdYjRwx17-8ZZmyB4,17017
6
+ rubka/context.py,sha256=jtdMYf2slFxK2kmkVO6MMSTl-yu5NH52UvzAOyB79hw,19317
7
7
  rubka/decorators.py,sha256=hGwUoE4q2ImrunJIGJ_kzGYYxQf1ueE0isadqraKEts,1157
8
8
  rubka/exceptions.py,sha256=tujZt1XrhWaw-lmdeVadVceUptpw4XzNgE44sAAY0gs,90
9
9
  rubka/jobs.py,sha256=GvLMLsVhcSEzRTgkvnPISPEBN71suW2xXI0hUaUZPTo,378
@@ -33,7 +33,7 @@ rubka/adaptorrubka/types/socket/message.py,sha256=0WgLMZh4eow8Zn7AiSX4C3GZjQTkIg
33
33
  rubka/adaptorrubka/utils/__init__.py,sha256=OgCFkXdNFh379quNwIVOAWY2NP5cIOxU5gDRRALTk4o,54
34
34
  rubka/adaptorrubka/utils/configs.py,sha256=nMUEOJh1NqDJsf9W9PurkN_DLYjO6kKPMm923i4Jj_A,492
35
35
  rubka/adaptorrubka/utils/utils.py,sha256=5-LioLNYX_TIbQGDeT50j7Sg9nAWH2LJUUs-iEXpsUY,8816
36
- rubka-4.5.2.dist-info/METADATA,sha256=xgwDJfrcrmaI2Hr9YEugIt8_3OzJiAXBHx2sldoW7ZU,33216
37
- rubka-4.5.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
- rubka-4.5.2.dist-info/top_level.txt,sha256=vy2A4lot11cRMdQS-F4HDCIXL3JK8RKfu7HMDkezJW4,6
39
- rubka-4.5.2.dist-info/RECORD,,
36
+ rubka-4.6.0.dist-info/METADATA,sha256=ZSH799BnofV3TFaXKdi_unkoru9Yki1Sbv3L1upObMI,33335
37
+ rubka-4.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
+ rubka-4.6.0.dist-info/top_level.txt,sha256=vy2A4lot11cRMdQS-F4HDCIXL3JK8RKfu7HMDkezJW4,6
39
+ rubka-4.6.0.dist-info/RECORD,,
File without changes