multibotkit 0.1.34__py3-none-any.whl → 0.2.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.
@@ -179,14 +179,14 @@ are required"
179
179
 
180
180
  def sync_set_webhook(self, webhook_data: SetWebhook):
181
181
  url = self.VIBER_BASE_URL + "set_webhook"
182
- data = webhook_data.dict(exclude_none=True)
182
+ data = webhook_data.model_dump(exclude_none=True)
183
183
  data["auth_token"] = self.token
184
184
  r = self._perform_sync_request(url=url, data=data)
185
185
  return r
186
186
 
187
187
  async def async_set_webhook(self, webhook_data: SetWebhook):
188
188
  url = self.VIBER_BASE_URL + "set_webhook"
189
- data = webhook_data.dict(exclude_none=True)
189
+ data = webhook_data.model_dump(exclude_none=True)
190
190
  data["auth_token"] = self.token
191
191
  r = await self._perform_async_request(url=url, data=data)
192
192
  return r
@@ -238,7 +238,7 @@ are required"
238
238
  )
239
239
 
240
240
  url = self.VIBER_BASE_URL + "send_message"
241
- data = message.dict(exclude_none=True)
241
+ data = message.model_dump(exclude_none=True)
242
242
  data["auth_token"] = self.token
243
243
  r = self._perform_sync_request(url=url, data=data)
244
244
  return r
@@ -280,7 +280,7 @@ are required"
280
280
  )
281
281
 
282
282
  url = self.VIBER_BASE_URL + "send_message"
283
- data = message.dict(exclude_none=True)
283
+ data = message.model_dump(exclude_none=True)
284
284
  data["auth_token"] = self.token
285
285
  r = await self._perform_async_request(url=url, data=data)
286
286
  return r
multibotkit/helpers/vk.py CHANGED
@@ -67,7 +67,7 @@ class VKHelper(BaseHelper):
67
67
  template=template,
68
68
  )
69
69
 
70
- data = message.dict(exclude_none=True)
70
+ data = message.model_dump(exclude_none=True)
71
71
  if data.get("keyboard"):
72
72
  data["keyboard"] = json.dumps(data["keyboard"], ensure_ascii=False)
73
73
  if data.get("template"):
@@ -101,7 +101,7 @@ but not both"
101
101
  attachment=attachment,
102
102
  template=template,
103
103
  )
104
- data = message.dict(exclude_none=True)
104
+ data = message.model_dump(exclude_none=True)
105
105
  if data.get("keyboard"):
106
106
  data["keyboard"] = json.dumps(data["keyboard"], ensure_ascii=False)
107
107
  if data.get("template"):
@@ -0,0 +1,408 @@
1
+ from json import JSONDecodeError
2
+ from typing import IO, List, Optional, Union
3
+
4
+ import httpx
5
+ from tenacity import (
6
+ retry,
7
+ retry_if_exception_type,
8
+ stop_after_attempt,
9
+ wait_exponential,
10
+ )
11
+
12
+ from multibotkit.helpers.base_helper import BaseHelper
13
+ from multibotkit.schemas.yandexmessenger.incoming import Update
14
+ from multibotkit.schemas.yandexmessenger.outgoing import (
15
+ GetUpdatesParams,
16
+ InlineKeyboard,
17
+ SendFileParams,
18
+ SendImageParams,
19
+ SendTextParams,
20
+ SetWebhookParams,
21
+ )
22
+
23
+
24
+ class YandexMessengerHelper(BaseHelper):
25
+ """
26
+ Sync и async функции для Yandex Messenger Bot API.
27
+
28
+ Базовый URL: https://botapi.messenger.yandex.net/bot/v1/
29
+ Авторизация: Authorization: OAuth <token>
30
+ """
31
+
32
+ def __init__(self, token: str):
33
+ self.token = token
34
+ self.base_url = "https://botapi.messenger.yandex.net/bot/v1/"
35
+ self.headers = {"Authorization": f"OAuth {self.token}"}
36
+
37
+ @retry(
38
+ retry=retry_if_exception_type(httpx.HTTPError)
39
+ | retry_if_exception_type(JSONDecodeError),
40
+ reraise=True,
41
+ stop=stop_after_attempt(5),
42
+ wait=wait_exponential(multiplier=1, min=4, max=10),
43
+ )
44
+ def _perform_sync_request(
45
+ self,
46
+ url: str,
47
+ data: Optional[dict] = None,
48
+ use_json: bool = True,
49
+ files: Optional[dict] = None,
50
+ ):
51
+ """Переопределение для добавления OAuth заголовка"""
52
+ if use_json:
53
+ r = httpx.post(url=url, json=data, headers=self.headers)
54
+ else:
55
+ r = httpx.post(url=url, data=data, files=files, headers=self.headers)
56
+ return r.json()
57
+
58
+ @retry(
59
+ retry=retry_if_exception_type(httpx.HTTPError)
60
+ | retry_if_exception_type(JSONDecodeError),
61
+ reraise=True,
62
+ stop=stop_after_attempt(5),
63
+ wait=wait_exponential(multiplier=1, min=4, max=10),
64
+ )
65
+ async def _perform_async_request(
66
+ self,
67
+ url: str,
68
+ data: Optional[dict] = None,
69
+ use_json: bool = True,
70
+ files: Optional[dict] = None,
71
+ ):
72
+ """Переопределение для добавления OAuth заголовка"""
73
+ async with httpx.AsyncClient() as client:
74
+ if use_json:
75
+ r = await client.post(url=url, json=data, headers=self.headers)
76
+ else:
77
+ r = await client.post(
78
+ url=url, data=data, files=files, headers=self.headers
79
+ )
80
+ return r.json()
81
+
82
+ def sync_send_text(
83
+ self,
84
+ text: str,
85
+ chat_id: Optional[str] = None,
86
+ login: Optional[str] = None,
87
+ payload_id: Optional[str] = None,
88
+ reply_message_id: Optional[int] = None,
89
+ disable_notification: Optional[bool] = None,
90
+ important: Optional[bool] = None,
91
+ disable_web_page_preview: Optional[bool] = None,
92
+ thread_id: Optional[int] = None,
93
+ inline_keyboard: Optional[InlineKeyboard] = None,
94
+ ) -> dict:
95
+ """
96
+ Синхронная отправка текстового сообщения.
97
+
98
+ Args:
99
+ text: Текст сообщения (до 6000 символов)
100
+ chat_id: ID чата (для group/channel)
101
+ login: Login пользователя (для private)
102
+ payload_id: Уникальный ID для дедупликации
103
+ reply_message_id: ID сообщения для ответа
104
+ disable_notification: Отключить уведомление
105
+ important: Отметить как важное
106
+ disable_web_page_preview: Отключить превью ссылок
107
+ thread_id: ID треда
108
+ inline_keyboard: Inline клавиатура
109
+
110
+ Returns:
111
+ dict с полями {"ok": true, "message_id": integer}
112
+ """
113
+ url = self.base_url + "messages/sendText/"
114
+ params = SendTextParams(
115
+ text=text,
116
+ chat_id=chat_id,
117
+ login=login,
118
+ payload_id=payload_id,
119
+ reply_message_id=reply_message_id,
120
+ disable_notification=disable_notification,
121
+ important=important,
122
+ disable_web_page_preview=disable_web_page_preview,
123
+ thread_id=thread_id,
124
+ inline_keyboard=inline_keyboard,
125
+ )
126
+ data = params.model_dump(exclude_none=True)
127
+
128
+ # Сериализация inline_keyboard если есть
129
+ if inline_keyboard:
130
+ data["inline_keyboard"] = inline_keyboard.model_dump(exclude_none=True)[
131
+ "buttons"
132
+ ]
133
+
134
+ return self._perform_sync_request(url, data)
135
+
136
+ async def async_send_text(
137
+ self,
138
+ text: str,
139
+ chat_id: Optional[str] = None,
140
+ login: Optional[str] = None,
141
+ payload_id: Optional[str] = None,
142
+ reply_message_id: Optional[int] = None,
143
+ disable_notification: Optional[bool] = None,
144
+ important: Optional[bool] = None,
145
+ disable_web_page_preview: Optional[bool] = None,
146
+ thread_id: Optional[int] = None,
147
+ inline_keyboard: Optional[InlineKeyboard] = None,
148
+ ) -> dict:
149
+ """Асинхронная версия sync_send_text"""
150
+ url = self.base_url + "messages/sendText/"
151
+ params = SendTextParams(
152
+ text=text,
153
+ chat_id=chat_id,
154
+ login=login,
155
+ payload_id=payload_id,
156
+ reply_message_id=reply_message_id,
157
+ disable_notification=disable_notification,
158
+ important=important,
159
+ disable_web_page_preview=disable_web_page_preview,
160
+ thread_id=thread_id,
161
+ inline_keyboard=inline_keyboard,
162
+ )
163
+ data = params.model_dump(exclude_none=True)
164
+
165
+ if inline_keyboard:
166
+ data["inline_keyboard"] = inline_keyboard.model_dump(exclude_none=True)
167
+
168
+ return await self._perform_async_request(url, data)
169
+
170
+ def sync_send_image(
171
+ self,
172
+ image: Union[str, IO],
173
+ chat_id: Optional[str] = None,
174
+ login: Optional[str] = None,
175
+ thread_id: Optional[int] = None,
176
+ ) -> dict:
177
+ """
178
+ Синхронная отправка изображения.
179
+
180
+ Args:
181
+ image: Путь к файлу, file_id или IO объект
182
+ chat_id: ID чата (для group/channel)
183
+ login: Login пользователя (для private)
184
+ thread_id: ID треда
185
+
186
+ Returns:
187
+ dict с полями {"ok": true, "message_id": integer}
188
+ """
189
+ url = self.base_url + "messages/sendImage/"
190
+ params = SendImageParams(
191
+ chat_id=chat_id,
192
+ login=login,
193
+ thread_id=thread_id,
194
+ )
195
+ data = params.model_dump(exclude_none=True)
196
+
197
+ # Обработка различных типов image
198
+ if isinstance(image, str):
199
+ # Если это путь к файлу с расширением
200
+ if image.endswith((".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp")):
201
+ with open(image, "rb") as f:
202
+ files = {"image": f}
203
+ return self._perform_sync_request(
204
+ url, data, use_json=False, files=files
205
+ )
206
+ else:
207
+ # Если это file_id - отправляем как JSON
208
+ data["image"] = image
209
+ return self._perform_sync_request(url, data)
210
+ else:
211
+ # Если это IO объект
212
+ files = {"image": image}
213
+ return self._perform_sync_request(url, data, use_json=False, files=files)
214
+
215
+ async def async_send_image(
216
+ self,
217
+ image: Union[str, IO],
218
+ chat_id: Optional[str] = None,
219
+ login: Optional[str] = None,
220
+ thread_id: Optional[int] = None,
221
+ ) -> dict:
222
+ """Асинхронная версия sync_send_image"""
223
+ url = self.base_url + "messages/sendImage/"
224
+ params = SendImageParams(
225
+ chat_id=chat_id,
226
+ login=login,
227
+ thread_id=thread_id,
228
+ )
229
+ data = params.model_dump(exclude_none=True)
230
+
231
+ if isinstance(image, str):
232
+ if image.endswith((".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp")):
233
+ with open(image, "rb") as f:
234
+ content = f.read()
235
+ files = {"image": content}
236
+ return await self._perform_async_request(
237
+ url, data, use_json=False, files=files
238
+ )
239
+ else:
240
+ data["image"] = image
241
+ return await self._perform_async_request(url, data)
242
+ else:
243
+ files = {"image": image}
244
+ return await self._perform_async_request(
245
+ url, data, use_json=False, files=files
246
+ )
247
+
248
+ def sync_send_file(
249
+ self,
250
+ document: Union[str, IO],
251
+ filename: Optional[str] = None,
252
+ chat_id: Optional[str] = None,
253
+ login: Optional[str] = None,
254
+ thread_id: Optional[int] = None,
255
+ ) -> dict:
256
+ """
257
+ Синхронная отправка файла.
258
+
259
+ Args:
260
+ document: Путь к файлу, file_id или IO объект
261
+ filename: Имя файла (для IO объектов)
262
+ chat_id: ID чата (для group/channel)
263
+ login: Login пользователя (для private)
264
+ thread_id: ID треда
265
+
266
+ Returns:
267
+ dict с полями {"ok": true, "message_id": integer}
268
+ """
269
+ url = self.base_url + "messages/sendFile/"
270
+ params = SendFileParams(
271
+ chat_id=chat_id,
272
+ login=login,
273
+ thread_id=thread_id,
274
+ )
275
+ data = params.model_dump(exclude_none=True)
276
+
277
+ if isinstance(document, str):
278
+ # Если это путь к файлу
279
+ if not document.startswith(("http://", "https://")):
280
+ # Локальный файл
281
+ fname = filename or document.split("/")[-1]
282
+ with open(document, "rb") as f:
283
+ files = {"document": (fname, f)}
284
+ return self._perform_sync_request(
285
+ url, data, use_json=False, files=files
286
+ )
287
+ else:
288
+ # file_id или URL
289
+ data["document"] = document
290
+ return self._perform_sync_request(url, data)
291
+ else:
292
+ # IO объект
293
+ fname = filename or "file"
294
+ files = {"document": (fname, document)}
295
+ return self._perform_sync_request(url, data, use_json=False, files=files)
296
+
297
+ async def async_send_file(
298
+ self,
299
+ document: Union[str, IO],
300
+ filename: Optional[str] = None,
301
+ chat_id: Optional[str] = None,
302
+ login: Optional[str] = None,
303
+ thread_id: Optional[int] = None,
304
+ ) -> dict:
305
+ """Асинхронная версия sync_send_file"""
306
+ url = self.base_url + "messages/sendFile/"
307
+ params = SendFileParams(
308
+ chat_id=chat_id,
309
+ login=login,
310
+ thread_id=thread_id,
311
+ )
312
+ data = params.model_dump(exclude_none=True)
313
+
314
+ if isinstance(document, str):
315
+ if not document.startswith(("http://", "https://")):
316
+ fname = filename or document.split("/")[-1]
317
+ with open(document, "rb") as f:
318
+ content = f.read()
319
+ files = {"document": (fname, content)}
320
+ return await self._perform_async_request(
321
+ url, data, use_json=False, files=files
322
+ )
323
+ else:
324
+ data["document"] = document
325
+ return await self._perform_async_request(url, data)
326
+ else:
327
+ fname = filename or "file"
328
+ files = {"document": (fname, document)}
329
+ return await self._perform_async_request(
330
+ url, data, use_json=False, files=files
331
+ )
332
+
333
+ def sync_get_updates(
334
+ self,
335
+ limit: Optional[int] = 100,
336
+ offset: Optional[int] = 0,
337
+ ) -> dict:
338
+ """
339
+ Синхронное получение обновлений (long polling).
340
+
341
+ Args:
342
+ limit: Количество обновлений (1-1000, по умолчанию 100)
343
+ offset: Смещение для пагинации
344
+
345
+ Returns:
346
+ dict с полями {"ok": true, "updates": [...]}
347
+ """
348
+ url = self.base_url + "messages/getUpdates/"
349
+ params = GetUpdatesParams(limit=limit, offset=offset)
350
+ data = params.model_dump(exclude_none=True)
351
+
352
+ return self._perform_sync_request(url, data)
353
+
354
+ async def async_get_updates(
355
+ self,
356
+ limit: Optional[int] = 100,
357
+ offset: Optional[int] = 0,
358
+ ) -> dict:
359
+ """Асинхронная версия sync_get_updates"""
360
+ url = self.base_url + "messages/getUpdates/"
361
+ params = GetUpdatesParams(limit=limit, offset=offset)
362
+ data = params.model_dump(exclude_none=True)
363
+
364
+ return await self._perform_async_request(url, data)
365
+
366
+ def sync_set_webhook(
367
+ self,
368
+ webhook_url: Optional[str] = None,
369
+ ) -> dict:
370
+ """
371
+ Синхронная установка webhook.
372
+
373
+ Args:
374
+ webhook_url: URL для webhook (None для отключения)
375
+
376
+ Returns:
377
+ dict с результатом операции
378
+ """
379
+ url = self.base_url + "self/update/"
380
+ params = SetWebhookParams(webhook_url=webhook_url)
381
+ data = params.model_dump(exclude_none=True)
382
+
383
+ return self._perform_sync_request(url, data)
384
+
385
+ async def async_set_webhook(
386
+ self,
387
+ webhook_url: Optional[str] = None,
388
+ ) -> dict:
389
+ """Асинхронная версия sync_set_webhook"""
390
+ url = self.base_url + "self/update/"
391
+ params = SetWebhookParams(webhook_url=webhook_url)
392
+ data = params.model_dump(exclude_none=True)
393
+
394
+ return await self._perform_async_request(url, data)
395
+
396
+ def parse_updates(self, response: dict) -> List[Update]:
397
+ """
398
+ Парсинг ответа getUpdates в список Update объектов.
399
+
400
+ Args:
401
+ response: dict ответ от getUpdates
402
+
403
+ Returns:
404
+ List[Update]
405
+ """
406
+ if response.get("ok") and "updates" in response:
407
+ return [Update.model_validate(update) for update in response["updates"]]
408
+ return []
@@ -1,7 +1,7 @@
1
1
  from enum import Enum
2
2
  from typing import Optional, List
3
3
 
4
- from pydantic import Field
4
+ from pydantic import ConfigDict, Field
5
5
  from pydantic.main import BaseModel
6
6
 
7
7
 
@@ -237,10 +237,12 @@ class WebAppData(BaseModel):
237
237
 
238
238
 
239
239
  class Message(BaseModel):
240
+ model_config = ConfigDict(populate_by_name=True)
241
+
240
242
  message_id: int = Field(..., title="Unique message identifier inside this chat")
241
243
  date: int = Field(..., title="Date the message was sent in Unix time")
242
244
  from_: Optional[User] = Field(
243
- None, title="Sender, empty for messages sent to channels"
245
+ None, title="Sender, empty for messages sent to channels", alias="from"
244
246
  )
245
247
  chat: Optional[Chat] = Field(None, title="Conversation the message belongs to")
246
248
  text: Optional[str] = Field(None, title="The actual UTF-8 text of the message")
@@ -284,13 +286,12 @@ the location",
284
286
  None, title="Service message: data sent by a Web App"
285
287
  )
286
288
 
287
- class Config:
288
- fields = {"from_": "from"}
289
-
290
289
 
291
290
  class CallbackQuery(BaseModel):
291
+ model_config = ConfigDict(populate_by_name=True)
292
+
292
293
  id: str = Field(..., title="Unique identifier for this query")
293
- from_: Optional[User] = Field(None, title="Sender data")
294
+ from_: Optional[User] = Field(None, title="Sender data", alias="from")
294
295
  message: Optional[Message] = Field(
295
296
  None, title="Message with the callback button that originated the query"
296
297
  )
@@ -311,9 +312,6 @@ which the message with the callback button was sent",
311
312
  unique identifier for the game",
312
313
  )
313
314
 
314
- class Config:
315
- fields = {"from_": "from"}
316
-
317
315
 
318
316
  class ChatMember(BaseModel):
319
317
  status: str = Field(..., title="The member's status in the chat")
@@ -368,9 +366,11 @@ class ChatInviteLink(BaseModel):
368
366
 
369
367
 
370
368
  class ChatMemberUpdated(BaseModel):
369
+ model_config = ConfigDict(populate_by_name=True)
370
+
371
371
  date: int = Field(..., title="Date the change was done in Unix time")
372
372
  from_: User = Field(
373
- ..., title="Performer of the action, which resulted in the change"
373
+ ..., title="Performer of the action, which resulted in the change", alias="from"
374
374
  )
375
375
  chat: Chat = Field(..., title="Chat the user belongs to")
376
376
  old_chat_member: ChatMember = Field(
@@ -393,13 +393,12 @@ class ChatMemberUpdated(BaseModel):
393
393
  title="Optional. True, if the user joined the chat via a chat folder invite link",
394
394
  )
395
395
 
396
- class Config:
397
- fields = {"from_": "from"}
398
-
399
396
 
400
397
  class ChatJoinRequest(BaseModel):
398
+ model_config = ConfigDict(populate_by_name=True)
399
+
401
400
  chat: Chat = Field(..., title=" Chat to which the request was sent")
402
- from_: User = Field(..., title="User that sent the join request")
401
+ from_: User = Field(..., title="User that sent the join request", alias="from")
403
402
  user_chat_id: int = Field(
404
403
  ...,
405
404
  title="Identifier of a private chat with the user who sent the join request.",
@@ -414,9 +413,6 @@ class ChatJoinRequest(BaseModel):
414
413
  title="Chat invite link that was used by the user to send the join request",
415
414
  )
416
415
 
417
- class Config:
418
- fields = {"from_": "from"}
419
-
420
416
 
421
417
  class Update(BaseModel):
422
418
  update_id: int = Field(..., title="Id of incoming bot update")
File without changes
@@ -0,0 +1,77 @@
1
+ from enum import Enum
2
+ from typing import List, Optional
3
+
4
+ from pydantic import BaseModel, ConfigDict, Field
5
+
6
+
7
+ class ChatType(str, Enum):
8
+ """Типы чатов в Yandex Messenger"""
9
+
10
+ private = "private"
11
+ group = "group"
12
+ channel = "channel"
13
+
14
+
15
+ class Sender(BaseModel):
16
+ """
17
+ Отправитель сообщения.
18
+ Либо login (для пользователей), либо id (для ботов/каналов).
19
+ """
20
+
21
+ login: Optional[str] = Field(None, title="Login пользователя")
22
+ id: Optional[str] = Field(None, title="ID отправителя (для ботов/каналов)")
23
+ display_name: Optional[str] = Field(None, title="Отображаемое имя")
24
+ robot: Optional[bool] = Field(None, title="Является ли отправитель роботом")
25
+
26
+
27
+ class Chat(BaseModel):
28
+ """
29
+ Информация о чате.
30
+ Для private чатов id отсутствует.
31
+ """
32
+
33
+ type: ChatType = Field(..., title="Тип чата: private/group/channel")
34
+ id: Optional[str] = Field(
35
+ None, title="ID чата (для group/channel, отсутствует для private)"
36
+ )
37
+
38
+
39
+ class Image(BaseModel):
40
+ """Изображение в сообщении"""
41
+
42
+ file_id: str = Field(..., title="ID файла для повторного использования")
43
+ width: int = Field(..., title="Ширина изображения в пикселях")
44
+ height: int = Field(..., title="Высота изображения в пикселях")
45
+ size: Optional[int] = Field(
46
+ None, title="Размер файла в байтах (опционально для оригиналов)"
47
+ )
48
+ name: Optional[str] = Field(None, title="Имя файла (опционально для оригиналов)")
49
+
50
+
51
+ class File(BaseModel):
52
+ """Файл в сообщении"""
53
+
54
+ id: str = Field(..., title="ID файла")
55
+ name: str = Field(..., title="Имя файла")
56
+ size: int = Field(..., title="Размер файла в байтах")
57
+
58
+
59
+ class Update(BaseModel):
60
+ """
61
+ Входящее обновление от Yandex Messenger.
62
+ Соответствует структуре getUpdates response и webhook payload.
63
+ """
64
+
65
+ update_id: int = Field(..., title="Уникальный ID обновления")
66
+ message_id: int = Field(..., title="ID сообщения")
67
+ timestamp: int = Field(..., title="Unix timestamp сообщения")
68
+ from_: Sender = Field(..., title="Отправитель сообщения", alias="from")
69
+ chat: Chat = Field(..., title="Информация о чате")
70
+ text: Optional[str] = Field(None, title="Текст сообщения (до 6000 символов)")
71
+ callback_data: Optional[dict] = Field(
72
+ None, title="Данные переданные по нажатию кнопки"
73
+ )
74
+ images: Optional[List[Image]] = Field(None, title="Массив изображений (если есть)")
75
+ file: Optional[File] = Field(None, title="Прикрепленный файл")
76
+
77
+ model_config = ConfigDict(populate_by_name=True)