maxapi-lite 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.
@@ -0,0 +1,42 @@
1
+ """
2
+ maxapi_lite - легковесный асинхронный клиент MAX Bot API, совместимый с aiohttp 3.8.
3
+
4
+ Позволяет использовать MAX API в проектах, привязанных к старому aiohttp
5
+ (например, aiogram 2.x требует aiohttp<3.9, а официальный maxapi - aiohttp>=3.12).
6
+ """
7
+
8
+ from .bot import Bot
9
+ from .enums import ChatPermission, ParseMode
10
+ from .exceptions import InvalidToken, MaxApiError
11
+ from .types import (
12
+ Attachment,
13
+ ButtonsPayload,
14
+ CallbackButton,
15
+ InputMedia,
16
+ LinkButton,
17
+ MessageButton,
18
+ RequestContactButton,
19
+ RequestGeoLocationButton,
20
+ )
21
+
22
+ __version__ = "0.2.0"
23
+
24
+ __all__ = [
25
+ # Main class
26
+ "Bot",
27
+ # Types
28
+ "Attachment",
29
+ "ButtonsPayload",
30
+ "CallbackButton",
31
+ "LinkButton",
32
+ "MessageButton",
33
+ "RequestContactButton",
34
+ "RequestGeoLocationButton",
35
+ "InputMedia",
36
+ # Enums
37
+ "ParseMode",
38
+ "ChatPermission",
39
+ # Exceptions
40
+ "MaxApiError",
41
+ "InvalidToken",
42
+ ]
maxapi_lite/bot.py ADDED
@@ -0,0 +1,453 @@
1
+ """
2
+ Max API Lite - легковесный клиент для Max API
3
+ Совместим с aiohttp 3.8.4
4
+ """
5
+
6
+ import mimetypes
7
+ import os
8
+ from typing import Any, Dict, List, Optional, Union
9
+
10
+ import aiohttp
11
+
12
+ from .client import MaxApiClient
13
+ from .enums import ParseMode
14
+ from .exceptions import MaxApiError
15
+ from .types import Attachment
16
+
17
+
18
+ class Bot:
19
+ """
20
+ Основной класс бота для работы с Max API
21
+ Реализует только необходимые методы для совместимости с aiohttp 3.8.4
22
+ """
23
+
24
+ def __init__(self, token: Optional[str] = None, parse_mode: Optional[ParseMode] = None):
25
+ """
26
+ Инициализация бота
27
+
28
+ Args:
29
+ token: Токен бота Max API
30
+ parse_mode: Режим парсинга по умолчанию (HTML/MARKDOWN)
31
+ """
32
+ if token is None:
33
+ token = os.environ.get("MAX_BOT_TOKEN")
34
+ if token is None:
35
+ raise ValueError("Token must be provided or set in MAX_BOT_TOKEN environment variable")
36
+
37
+ self.client = MaxApiClient(token)
38
+ self.parse_mode = parse_mode
39
+ self._me: Optional[Dict[str, Any]] = None
40
+
41
+ async def close_session(self):
42
+ """Закрыть HTTP сессию"""
43
+ await self.client.close()
44
+
45
+ async def get_me(self) -> Dict[str, Any]:
46
+ """
47
+ Получить информацию о текущем боте
48
+
49
+ Returns:
50
+ Dict с информацией о боте (user_id, name, username, etc)
51
+ """
52
+ if self._me is None:
53
+ self._me = await self.client.request("GET", "/me")
54
+ return self._me
55
+
56
+ async def get_updates(self, limit: int = 100, timeout: int = 30, marker: Optional[int] = None) -> Dict[str, Any]:
57
+ """
58
+ Получить обновления бота (long polling)
59
+
60
+ Args:
61
+ limit: Максимальное количество обновлений
62
+ timeout: Таймаут ожидания новых обновлений
63
+ marker: Маркер последнего обработанного обновления
64
+
65
+ Returns:
66
+ Dict с полем "updates" - список обновлений
67
+ """
68
+ params = {"limit": limit, "timeout": timeout}
69
+ if marker is not None:
70
+ params["marker"] = marker
71
+
72
+ return await self.client.request("GET", "/updates", params=params)
73
+
74
+ @staticmethod
75
+ def _serialize_attachments(
76
+ attachments: Optional[List[Union[Attachment, Dict[str, Any]]]],
77
+ ) -> Optional[List[Dict[str, Any]]]:
78
+ if attachments is None:
79
+ return None
80
+ result: List[Dict[str, Any]] = []
81
+ for att in attachments:
82
+ if hasattr(att, "model_dump"):
83
+ result.append(att.model_dump())
84
+ elif isinstance(att, dict):
85
+ result.append(att)
86
+ else:
87
+ result.append(att)
88
+ return result
89
+
90
+ @staticmethod
91
+ def extract_message_id(response: Optional[Dict[str, Any]]) -> Optional[str]:
92
+ if not isinstance(response, dict):
93
+ return None
94
+ message = response.get("message") or {}
95
+ body = message.get("body") or {}
96
+ mid = body.get("mid")
97
+ if not mid:
98
+ return None
99
+ return str(mid)
100
+
101
+ @staticmethod
102
+ def build_link_button_attachment(text: str, url: str) -> Dict[str, Any]:
103
+ return {
104
+ "type": "inline_keyboard",
105
+ "payload": {
106
+ "buttons": [
107
+ [
108
+ {
109
+ "type": "link",
110
+ "text": text,
111
+ "url": url,
112
+ }
113
+ ]
114
+ ]
115
+ },
116
+ }
117
+
118
+ @staticmethod
119
+ def _media_mapping(media_type: str) -> tuple[str, str]:
120
+ return {
121
+ "photo": ("image", "image"),
122
+ "animation": ("image", "image"),
123
+ "video": ("video", "video"),
124
+ "document": ("file", "file"),
125
+ "file": ("file", "file"),
126
+ "image": ("image", "image"),
127
+ }.get(media_type, ("file", "file"))
128
+
129
+ async def upload_file(self, file_path: str, upload_type: str) -> Dict[str, Any]:
130
+ if not file_path or not os.path.isfile(file_path):
131
+ raise MaxApiError(f"File not found: {file_path}", 400)
132
+
133
+ init_resp = await self.client.request("POST", "/uploads", params={"type": upload_type})
134
+ upload_url = init_resp.get("url")
135
+ if not upload_url:
136
+ raise MaxApiError(f"MAX upload init response has no url: {init_resp}", 500)
137
+
138
+ content_type = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
139
+ async with aiohttp.ClientSession() as session:
140
+ with open(file_path, "rb") as media_file:
141
+ form = aiohttp.FormData()
142
+ form.add_field(
143
+ "data",
144
+ media_file,
145
+ filename=os.path.basename(file_path),
146
+ content_type=content_type,
147
+ )
148
+ async with session.post(upload_url, data=form) as response:
149
+ if response.status >= 400:
150
+ text = await response.text()
151
+ raise MaxApiError(f"MAX upload failed [{response.status}]: {text}", response.status)
152
+ try:
153
+ return await response.json()
154
+ except Exception:
155
+ return {}
156
+
157
+ async def create_media_attachment(
158
+ self,
159
+ media_type: str,
160
+ *,
161
+ file_path: Optional[str] = None,
162
+ url: Optional[str] = None,
163
+ ) -> Optional[Dict[str, Any]]:
164
+ upload_type, attachment_type = self._media_mapping(media_type)
165
+ if url:
166
+ return {"type": attachment_type, "payload": {"url": url}}
167
+ if not file_path:
168
+ return None
169
+
170
+ upload_resp = await self.upload_file(file_path, upload_type)
171
+ if upload_type == "image":
172
+ photos = upload_resp.get("photos") or {}
173
+ first_photo = next(iter(photos.values()), None) if isinstance(photos, dict) else None
174
+ if not isinstance(first_photo, dict):
175
+ raise MaxApiError(f"MAX image upload payload is invalid: {upload_resp}", 500)
176
+ payload = first_photo
177
+ else:
178
+ payload = upload_resp if isinstance(upload_resp, dict) else {}
179
+ if not payload.get("token"):
180
+ raise MaxApiError(f"MAX upload payload is invalid: {upload_resp}", 500)
181
+ return {"type": attachment_type, "payload": payload}
182
+
183
+ async def send_message(
184
+ self,
185
+ chat_id: Optional[int] = None,
186
+ user_id: Optional[int] = None,
187
+ text: Optional[str] = None,
188
+ attachments: Optional[List[Union[Attachment, Dict[str, Any]]]] = None,
189
+ format: Optional[str] = None,
190
+ notify: Optional[bool] = None,
191
+ **kwargs,
192
+ ) -> Dict[str, Any]:
193
+ """
194
+ Отправить сообщение
195
+
196
+ Args:
197
+ chat_id: ID чата (для групповых чатов)
198
+ user_id: ID пользователя (для личных сообщений)
199
+ text: Текст сообщения
200
+ attachments: Вложения (кнопки, медиа)
201
+ format: Формат текста (html/markdown)
202
+ notify: Отправлять уведомление
203
+
204
+ Returns:
205
+ Dict с отправленным сообщением
206
+ """
207
+ # Формируем тело запроса
208
+ body: Dict[str, Any] = {}
209
+ params: Dict[str, Any] = {}
210
+
211
+ if chat_id is not None:
212
+ params["chat_id"] = chat_id
213
+ elif user_id is not None:
214
+ params["user_id"] = user_id
215
+ else:
216
+ raise ValueError("Either chat_id or user_id must be provided")
217
+
218
+ if text:
219
+ body["text"] = text
220
+
221
+ serialized_attachments = self._serialize_attachments(attachments)
222
+ if serialized_attachments is not None:
223
+ body["attachments"] = serialized_attachments
224
+
225
+ # Используем format или parse_mode
226
+ if format:
227
+ body["format"] = format
228
+ elif self.parse_mode:
229
+ body["format"] = self.parse_mode.value
230
+
231
+ if notify is not None:
232
+ body["notify"] = notify
233
+
234
+ return await self.client.request("POST", "/messages", params=params, json=body)
235
+
236
+ async def send_message_with_media(
237
+ self,
238
+ *,
239
+ chat_id: int,
240
+ text: Optional[str] = None,
241
+ media_type: Optional[str] = None,
242
+ file_path: Optional[str] = None,
243
+ media_url: Optional[str] = None,
244
+ attachments: Optional[List[Union[Attachment, Dict[str, Any]]]] = None,
245
+ format: Optional[str] = None,
246
+ notify: Optional[bool] = None,
247
+ ) -> Dict[str, Any]:
248
+ send_attachments: List[Union[Attachment, Dict[str, Any]]] = []
249
+ if attachments:
250
+ send_attachments.extend(attachments)
251
+ if media_type and (file_path or media_url):
252
+ media_attachment = await self.create_media_attachment(media_type, file_path=file_path, url=media_url)
253
+ if media_attachment:
254
+ send_attachments.append(media_attachment)
255
+ return await self.send_message(
256
+ chat_id=chat_id,
257
+ text=text,
258
+ attachments=send_attachments or None,
259
+ format=format,
260
+ notify=notify,
261
+ )
262
+
263
+ async def edit_message(
264
+ self,
265
+ message_id: str,
266
+ text: Optional[str] = None,
267
+ attachments: Optional[List[Union[Attachment, Dict[str, Any]]]] = None,
268
+ format: Optional[str] = None,
269
+ **kwargs,
270
+ ) -> Dict[str, Any]:
271
+ """
272
+ Редактировать сообщение
273
+
274
+ Args:
275
+ message_id: ID сообщения для редактирования
276
+ text: Новый текст
277
+ attachments: Новые вложения
278
+
279
+ Returns:
280
+ Dict с отредактированным сообщением
281
+ """
282
+ body: Dict[str, Any] = {}
283
+ params: Dict[str, Any] = {"message_id": message_id}
284
+
285
+ if text is not None:
286
+ body["text"] = text
287
+
288
+ serialized_attachments = self._serialize_attachments(attachments)
289
+ if serialized_attachments is not None:
290
+ body["attachments"] = serialized_attachments
291
+
292
+ # Preserve text formatting on edits the same way as in send_message.
293
+ # Format only applies to text, so attachment-only edits leave the text untouched.
294
+ if text is not None:
295
+ if format:
296
+ body["format"] = format
297
+ elif self.parse_mode:
298
+ body["format"] = self.parse_mode.value
299
+
300
+ return await self.client.request("PUT", "/messages", params=params, json=body)
301
+
302
+ async def edit_message_with_media(
303
+ self,
304
+ *,
305
+ message_id: str,
306
+ text: Optional[str] = None,
307
+ media_type: Optional[str] = None,
308
+ file_path: Optional[str] = None,
309
+ media_url: Optional[str] = None,
310
+ attachments: Optional[List[Union[Attachment, Dict[str, Any]]]] = None,
311
+ format: Optional[str] = None,
312
+ ) -> Dict[str, Any]:
313
+ edit_attachments: List[Union[Attachment, Dict[str, Any]]] = []
314
+ if attachments:
315
+ edit_attachments.extend(attachments)
316
+ if media_type and (file_path or media_url):
317
+ media_attachment = await self.create_media_attachment(media_type, file_path=file_path, url=media_url)
318
+ if media_attachment:
319
+ edit_attachments.append(media_attachment)
320
+ return await self.edit_message(
321
+ message_id=message_id,
322
+ text=text,
323
+ attachments=edit_attachments or [],
324
+ format=format,
325
+ )
326
+
327
+ async def pin_chat_message(self, chat_id: int, message_id: str, notify: bool = True) -> Dict[str, Any]:
328
+ """
329
+ Закрепить сообщение в чате/канале.
330
+
331
+ Args:
332
+ chat_id: ID чата
333
+ message_id: ID сообщения (mid.*)
334
+ notify: Показывать уведомление участникам
335
+ """
336
+ body: Dict[str, Any] = {"message_id": message_id}
337
+ if notify is not None:
338
+ body["notify"] = notify
339
+ return await self.client.request("PUT", f"/chats/{chat_id}/pin", json=body)
340
+
341
+ async def pin_from_response(
342
+ self, chat_id: int, response: Dict[str, Any], notify: bool = True
343
+ ) -> Optional[Dict[str, Any]]:
344
+ message_id = self.extract_message_id(response)
345
+ if not message_id:
346
+ return None
347
+ return await self.pin_chat_message(chat_id=chat_id, message_id=message_id, notify=notify)
348
+
349
+ async def get_message(self, message_id: str) -> Dict[str, Any]:
350
+ """Получить сообщение (текст и вложения) по ID."""
351
+ return await self.client.request("GET", f"/messages/{message_id}")
352
+
353
+ async def unpin_chat_message(self, chat_id: int) -> Dict[str, Any]:
354
+ """Remove the pinned message or channel post from a MAX chat."""
355
+ return await self.client.request("DELETE", f"/chats/{chat_id}/pin")
356
+
357
+ async def delete_message(self, message_id: str) -> Dict[str, Any]:
358
+ """
359
+ Удалить сообщение
360
+
361
+ Args:
362
+ message_id: ID сообщения
363
+
364
+ Returns:
365
+ Dict с результатом удаления
366
+ """
367
+ return await self.client.request("DELETE", f"/messages/{message_id}")
368
+
369
+ async def send_callback(self, callback_id: str, notification: Optional[str] = None) -> Dict[str, Any]:
370
+ """
371
+ Ответить на callback query
372
+
373
+ Args:
374
+ callback_id: ID callback query
375
+ notification: Текст уведомления для пользователя
376
+
377
+ Returns:
378
+ Dict с результатом
379
+ """
380
+ body: Dict[str, Any] = {"callback_id": callback_id}
381
+
382
+ if notification:
383
+ body["notification"] = notification
384
+
385
+ return await self.client.request("POST", "/answers", json=body)
386
+
387
+ async def get_chat_by_id(self, id: int) -> Dict[str, Any]:
388
+ """
389
+ Получить информацию о чате
390
+
391
+ Args:
392
+ id: ID чата
393
+
394
+ Returns:
395
+ Dict с информацией о чате
396
+ """
397
+ return await self.client.request("GET", f"/chats/{id}")
398
+
399
+ async def get_chat_member(self, chat_id: int, user_id: int) -> Optional[Dict[str, Any]]:
400
+ """
401
+ Получить информацию об участнике чата с его правами
402
+
403
+ Args:
404
+ chat_id: ID чата
405
+ user_id: ID пользователя
406
+
407
+ Returns:
408
+ Dict с информацией об участнике (включая permissions) или None
409
+ """
410
+ # Max API возвращает список участников
411
+ result = await self.client.request("GET", f"/chats/{chat_id}/members", params={"user_ids": [user_id]})
412
+
413
+ members = result.get("members", [])
414
+ if members:
415
+ return members[0]
416
+
417
+ return None
418
+
419
+ async def get_list_admin_chat(self, chat_id: int) -> Dict[str, Any]:
420
+ """
421
+ Получить список администраторов чата
422
+
423
+ Args:
424
+ chat_id: ID чата
425
+
426
+ Returns:
427
+ Dict со списком администраторов в поле "members"
428
+ """
429
+ return await self.client.request("GET", f"/chats/{chat_id}/members/admins")
430
+
431
+ async def get_chat_members_batch(self, chat_id: int, user_ids: list[int]) -> list[int]:
432
+ """
433
+ Проверить список пользователей на членство в чате.
434
+ Возвращает список user_id которые состоят в чате.
435
+
436
+ Args:
437
+ chat_id: ID чата
438
+ user_ids: Список ID пользователей для проверки (максимум 100 за раз)
439
+
440
+ Returns:
441
+ List с user_id участников чата
442
+ """
443
+ if not user_ids:
444
+ return []
445
+
446
+ ids_str = ','.join(str(uid) for uid in user_ids)
447
+ result = await self.client.request(
448
+ "GET",
449
+ f"/chats/{chat_id}/members",
450
+ params={"user_ids": ids_str}
451
+ )
452
+ members = result.get("members", [])
453
+ return [m.get("user_id") for m in members if m.get("user_id")]
maxapi_lite/client.py ADDED
@@ -0,0 +1,90 @@
1
+ """
2
+ HTTP клиент для Max API Lite
3
+ """
4
+
5
+ import aiohttp
6
+ from typing import Optional, Dict, Any
7
+ from .exceptions import MaxApiError, InvalidToken
8
+
9
+
10
+ class MaxApiClient:
11
+ """Легковесный HTTP клиент для Max API, совместимый с aiohttp 3.8.4"""
12
+
13
+ BASE_URL = "https://platform-api.max.ru"
14
+
15
+ def __init__(self, token: str):
16
+ """
17
+ Инициализация клиента
18
+
19
+ Args:
20
+ token: Токен бота Max API
21
+ """
22
+ if not token:
23
+ raise InvalidToken("Token cannot be empty")
24
+
25
+ self.token = token
26
+ self.headers = {"Authorization": token}
27
+ self._session: Optional[aiohttp.ClientSession] = None
28
+
29
+ async def _get_session(self) -> aiohttp.ClientSession:
30
+ """Получить или создать aiohttp сессию"""
31
+ if self._session is None or self._session.closed:
32
+ self._session = aiohttp.ClientSession(headers=self.headers)
33
+ return self._session
34
+
35
+ async def close(self):
36
+ """Закрыть HTTP сессию"""
37
+ if self._session and not self._session.closed:
38
+ await self._session.close()
39
+ self._session = None
40
+
41
+ async def request(
42
+ self,
43
+ method: str,
44
+ endpoint: str,
45
+ params: Optional[Dict[str, Any]] = None,
46
+ json: Optional[Dict[str, Any]] = None,
47
+ **kwargs,
48
+ ) -> Dict[str, Any]:
49
+ """
50
+ Выполнить HTTP запрос к Max API
51
+
52
+ Args:
53
+ method: HTTP метод (GET, POST, PUT, DELETE)
54
+ endpoint: API endpoint (начинается с /)
55
+ params: Query параметры
56
+ json: JSON body запроса
57
+ **kwargs: Дополнительные параметры для aiohttp
58
+
59
+ Returns:
60
+ Dict с ответом API
61
+
62
+ Raises:
63
+ InvalidToken: При ошибке авторизации (401)
64
+ MaxApiError: При других ошибках API
65
+ """
66
+ session = await self._get_session()
67
+ url = f"{self.BASE_URL}{endpoint}"
68
+
69
+ try:
70
+ async with session.request(method=method, url=url, params=params, json=json, **kwargs) as response:
71
+ # Читаем ответ
72
+ try:
73
+ data = await response.json()
74
+ except Exception:
75
+ # Если не JSON, читаем как текст
76
+ text = await response.text()
77
+ data = {"error": text}
78
+
79
+ # Проверяем статус
80
+ if response.status == 401:
81
+ raise InvalidToken("Invalid or expired token")
82
+
83
+ if response.status >= 400:
84
+ error_message = data.get("error", data.get("message", "Unknown error"))
85
+ raise MaxApiError(error_message, response.status)
86
+
87
+ return data
88
+
89
+ except aiohttp.ClientError as e:
90
+ raise MaxApiError(f"Connection error: {e}", 500)
maxapi_lite/enums.py ADDED
@@ -0,0 +1,23 @@
1
+ """
2
+ Enums для Max API Lite
3
+ """
4
+
5
+ from enum import Enum
6
+
7
+
8
+ class ParseMode(str, Enum):
9
+ """Режим парсинга текста"""
10
+
11
+ HTML = "html"
12
+ MARKDOWN = "markdown"
13
+
14
+
15
+ class ChatPermission(str, Enum):
16
+ """Права участника чата"""
17
+
18
+ # Основные права из Max API
19
+ READ_ALL_MESSAGES = "read_all_messages"
20
+ WRITE = "write"
21
+ ADD_REMOVE_MEMBERS = "add_remove_members"
22
+ ADD_ADMINS = "add_admins"
23
+ CHANGE_CHAT_INFO = "change_chat_info"
@@ -0,0 +1,19 @@
1
+ """
2
+ Исключения для Max API Lite
3
+ """
4
+
5
+
6
+ class MaxApiError(Exception):
7
+ """Базовое исключение Max API"""
8
+
9
+ def __init__(self, message: str = "Unknown error", code: int = 500):
10
+ self.message = message
11
+ self.code = code
12
+ super().__init__(f"[{code}] {message}")
13
+
14
+
15
+ class InvalidToken(MaxApiError):
16
+ """Неверный токен авторизации"""
17
+
18
+ def __init__(self, message: str = "Invalid token"):
19
+ super().__init__(message, 401)
maxapi_lite/py.typed ADDED
File without changes
maxapi_lite/types.py ADDED
@@ -0,0 +1,64 @@
1
+ """
2
+ Типы данных для Max API Lite
3
+ """
4
+
5
+ from pydantic import BaseModel
6
+ from typing import Optional, List, Any, Union, Dict
7
+
8
+
9
+ # Кнопки
10
+ class CallbackButton(BaseModel):
11
+ """Кнопка с callback"""
12
+
13
+ text: str
14
+ payload: str
15
+
16
+
17
+ class LinkButton(BaseModel):
18
+ """Кнопка со ссылкой"""
19
+
20
+ text: str
21
+ url: str
22
+
23
+
24
+ class MessageButton(BaseModel):
25
+ """Кнопка с текстовым сообщением"""
26
+
27
+ text: str
28
+
29
+
30
+ class RequestContactButton(BaseModel):
31
+ """Кнопка запроса контакта"""
32
+
33
+ text: str
34
+
35
+
36
+ class RequestGeoLocationButton(BaseModel):
37
+ """Кнопка запроса геолокации"""
38
+
39
+ text: str
40
+
41
+
42
+ ButtonType = Union[CallbackButton, LinkButton, MessageButton, RequestContactButton, RequestGeoLocationButton]
43
+
44
+
45
+ class ButtonsPayload(BaseModel):
46
+ """Payload для кнопок"""
47
+
48
+ buttons: List[List[ButtonType]]
49
+
50
+
51
+ class Attachment(BaseModel):
52
+ """Вложение к сообщению"""
53
+
54
+ type: str
55
+ payload: Union[ButtonsPayload, Dict[str, Any], Any]
56
+
57
+
58
+ class InputMedia(BaseModel):
59
+ """Медиа для загрузки"""
60
+
61
+ type: str = "photo"
62
+ url: Optional[str] = None
63
+ token: Optional[str] = None
64
+ file_path: Optional[str] = None
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.5
2
+ Name: maxapi-lite
3
+ Version: 0.2.0
4
+ Summary: Lightweight async client for the MAX messenger Bot API, compatible with aiohttp 3.8
5
+ Project-URL: Homepage, https://github.com/kiriyk/maxapi-lite
6
+ Project-URL: Source, https://github.com/kiriyk/maxapi-lite
7
+ Project-URL: Issues, https://github.com/kiriyk/maxapi-lite/issues
8
+ Project-URL: Changelog, https://github.com/kiriyk/maxapi-lite/blob/main/CHANGELOG.md
9
+ Author: Kirill
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: aiohttp,asyncio,bot,bot-api,max,max.ru,messenger
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Framework :: AsyncIO
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Natural Language :: Russian
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Topic :: Communications :: Chat
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.9
29
+ Requires-Dist: aiohttp>=3.8
30
+ Requires-Dist: pydantic>=2
31
+ Provides-Extra: test
32
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'test'
33
+ Requires-Dist: pytest>=7; extra == 'test'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # maxapi-lite
37
+
38
+ [![PyPI](https://img.shields.io/pypi/v/maxapi-lite.svg)](https://pypi.org/project/maxapi-lite/)
39
+ [![Python](https://img.shields.io/pypi/pyversions/maxapi-lite.svg)](https://pypi.org/project/maxapi-lite/)
40
+ [![CI](https://github.com/kiriyk/maxapi-lite/actions/workflows/ci.yml/badge.svg)](https://github.com/kiriyk/maxapi-lite/actions/workflows/ci.yml)
41
+
42
+ Легковесный асинхронный клиент [MAX Bot API](https://dev.max.ru/docs-api), совместимый с `aiohttp 3.8.x`.
43
+
44
+ Пакет нужен, когда в проекте уже зафиксирован старый `aiohttp` — например, рядом живёт
45
+ `aiogram 2.x` (`aiohttp<3.9`), а официальный клиент `maxapi` требует `aiohttp>=3.12`.
46
+ Зависимости — только `aiohttp>=3.8` и `pydantic>=2`.
47
+
48
+ *Lightweight async client for the MAX messenger Bot API that works with aiohttp 3.8,
49
+ so it can share an environment with aiogram 2.x.*
50
+
51
+ ## Установка
52
+
53
+ ```bash
54
+ pip install maxapi-lite
55
+ ```
56
+
57
+ ## Быстрый старт
58
+
59
+ ```python
60
+ import asyncio
61
+ from maxapi_lite import Bot, ParseMode
62
+
63
+
64
+ async def main():
65
+ bot = Bot(token="YOUR_MAX_TOKEN", parse_mode=ParseMode.HTML) # или переменная окружения MAX_BOT_TOKEN
66
+
67
+ # Кнопка-ссылка
68
+ kb_attachment = bot.build_link_button_attachment(
69
+ text="Открыть MiniApp",
70
+ url="https://example.com/app?startapp=123",
71
+ )
72
+
73
+ # Отправка фото + кнопки
74
+ result = await bot.send_message_with_media(
75
+ chat_id=-70893181482276,
76
+ text="<b>Розыгрыш</b>",
77
+ media_type="photo",
78
+ file_path="banner.jpg", # или media_url="https://..."
79
+ attachments=[kb_attachment],
80
+ )
81
+
82
+ # Извлечение mid и закрепление
83
+ mid = bot.extract_message_id(result)
84
+ if mid:
85
+ await bot.pin_chat_message(chat_id=-70893181482276, message_id=mid)
86
+
87
+ await bot.close_session()
88
+
89
+
90
+ asyncio.run(main())
91
+ ```
92
+
93
+ ### Long polling
94
+
95
+ ```python
96
+ marker = None
97
+ while True:
98
+ data = await bot.get_updates(timeout=30, marker=marker)
99
+ for update in data.get("updates", []):
100
+ ...
101
+ marker = data.get("marker", marker)
102
+ ```
103
+
104
+ ## Методы `Bot`
105
+
106
+ | Группа | Методы |
107
+ | --- | --- |
108
+ | Бот и обновления | `get_me`, `get_updates` |
109
+ | Сообщения | `send_message`, `edit_message`, `delete_message`, `get_message` |
110
+ | Медиа | `upload_file`, `create_media_attachment`, `send_message_with_media`, `edit_message_with_media` |
111
+ | Закрепление | `pin_chat_message`, `pin_from_response`, `unpin_chat_message` |
112
+ | Чаты | `get_chat_by_id`, `get_chat_member`, `get_chat_members_batch`, `get_list_admin_chat` |
113
+ | Callback | `send_callback` |
114
+ | Утилиты | `extract_message_id`, `build_link_button_attachment` |
115
+
116
+ Методы возвращают «сырые» `dict` из ответа API. Ошибки API поднимаются как `MaxApiError`
117
+ (с полем `code`), ошибка авторизации — как `InvalidToken`.
118
+
119
+ ## Поддерживаемые `media_type`
120
+
121
+ - `photo`, `animation`, `image` → загрузка как `image`
122
+ - `video`
123
+ - `document`, `file`
124
+
125
+ ## Примечания
126
+
127
+ - `chat_id` / `user_id` передаются query-параметрами, как требует MAX API.
128
+ - `edit_message(..., attachments=[])` явно очищает вложения; `attachments=None` оставляет их как есть.
129
+ - Формат (`parse_mode`) применяется к редактированию только если передан `text`.
130
+ - Для upload используется multipart-поле `data`.
131
+
132
+ ## Разработка
133
+
134
+ ```bash
135
+ pip install -e ".[test]"
136
+ pytest
137
+ ```
138
+
139
+ Релиз: поднять `__version__` в `src/maxapi_lite/__init__.py`, дополнить `CHANGELOG.md`,
140
+ создать GitHub Release с тегом `vX.Y.Z` — workflow `publish.yml` соберёт пакет и опубликует его на PyPI.
141
+
142
+ ## Лицензия
143
+
144
+ MIT
@@ -0,0 +1,11 @@
1
+ maxapi_lite/__init__.py,sha256=a_qc31x08vLA3zWuESUrdaqPV7uacVKdbzKly28ll9o,1033
2
+ maxapi_lite/bot.py,sha256=wyHmt6QcvvjIYxrCa4kwNXhxLIY8AgXPEAV0g2Ho3VI,16773
3
+ maxapi_lite/client.py,sha256=SUROnI6SY292A9k03OZ3ZLyGqNObarXx6eeM_Z1phwU,3060
4
+ maxapi_lite/enums.py,sha256=bdsyuE5hQN_jd24MMob2R1sI9lChKveEJuUj6_xcLiY,497
5
+ maxapi_lite/exceptions.py,sha256=EjcUCpEOxYbQ2GTddelrR9ZMCkB2lxt4cK3aL_bNAcM,505
6
+ maxapi_lite/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ maxapi_lite/types.py,sha256=dPTi4INNRu6oS0PJ9U-K3tqGLeTeblrXG5P_F_mXe3k,1270
8
+ maxapi_lite-0.2.0.dist-info/METADATA,sha256=od-1KG-WO5FrKPEPR2xabsiN7TKdkvDdLTezmDsJMNw,5546
9
+ maxapi_lite-0.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
10
+ maxapi_lite-0.2.0.dist-info/licenses/LICENSE,sha256=FrhT0HHTK2qCPqAUkuZpq3h62GxeU5ZxJMn3UcRl2-g,1063
11
+ maxapi_lite-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kiriyk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.