maxapi-lite 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- maxapi_lite-0.3.0/CHANGELOG.md +24 -0
- maxapi_lite-0.3.0/PKG-INFO +299 -0
- maxapi_lite-0.3.0/README.md +264 -0
- {maxapi_lite-0.2.0 → maxapi_lite-0.3.0}/src/maxapi_lite/__init__.py +5 -2
- {maxapi_lite-0.2.0 → maxapi_lite-0.3.0}/src/maxapi_lite/bot.py +151 -37
- maxapi_lite-0.3.0/src/maxapi_lite/client.py +138 -0
- maxapi_lite-0.3.0/src/maxapi_lite/exceptions.py +84 -0
- maxapi_lite-0.3.0/src/maxapi_lite/transfers.py +179 -0
- {maxapi_lite-0.2.0 → maxapi_lite-0.3.0}/src/maxapi_lite/types.py +6 -1
- maxapi_lite-0.3.0/tests/conftest.py +17 -0
- maxapi_lite-0.3.0/tests/test_bot.py +292 -0
- maxapi_lite-0.3.0/tests/test_client.py +180 -0
- maxapi_lite-0.3.0/tests/test_pipeline.py +69 -0
- maxapi_lite-0.3.0/tests/test_transfers.py +408 -0
- maxapi_lite-0.3.0/tests/test_types.py +31 -0
- maxapi_lite-0.2.0/CHANGELOG.md +0 -12
- maxapi_lite-0.2.0/PKG-INFO +0 -144
- maxapi_lite-0.2.0/README.md +0 -109
- maxapi_lite-0.2.0/src/maxapi_lite/client.py +0 -90
- maxapi_lite-0.2.0/src/maxapi_lite/exceptions.py +0 -19
- maxapi_lite-0.2.0/tests/test_bot.py +0 -128
- maxapi_lite-0.2.0/tests/test_client.py +0 -63
- {maxapi_lite-0.2.0 → maxapi_lite-0.3.0}/.gitignore +0 -0
- {maxapi_lite-0.2.0 → maxapi_lite-0.3.0}/LICENSE +0 -0
- {maxapi_lite-0.2.0 → maxapi_lite-0.3.0}/pyproject.toml +0 -0
- {maxapi_lite-0.2.0 → maxapi_lite-0.3.0}/src/maxapi_lite/enums.py +0 -0
- {maxapi_lite-0.2.0 → maxapi_lite-0.3.0}/src/maxapi_lite/py.typed +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- Новый API-домен `platform-api2.max.ru`; параметры `base_url`, `ssl_context`, `connector`, `timeout` и `transfer_timeout` в `Bot` и `MaxApiClient`.
|
|
6
|
+
- Отдельная сессия без Authorization для upload/download, общий настроенный TLS и явное владение connector.
|
|
7
|
+
- Исправлен token из `/uploads` для video/audio; добавлены audio mapping и проверка размера файла до сетевого запроса.
|
|
8
|
+
- `download_file`: потоковая запись, проверка размера, докачка через Range/If-Range и атомарная замена готового файла.
|
|
9
|
+
- `get_video`, `extract_attachments`, фильтр `types` и согласованный HTTP timeout для `get_updates`.
|
|
10
|
+
- `FileTooLarge`, `AttachmentNotReady`, `RateLimited`; строковый код MAX доступен в `MaxApiError.api_code`, числовой HTTP status остаётся в `.code`.
|
|
11
|
+
- Опциональное ожидание обработки вложений через `wait_attachments=True` с общим сроком 60 секунд. Сетевые ошибки отправки и 429 автоматически не повторяются.
|
|
12
|
+
- Поле `type` во всех моделях кнопок, `build_keyboard`, `send_callback(message=...)`; `callback_id` передаётся в query.
|
|
13
|
+
- CI проверяет aiohttp 3.9.5 дополнительно к 3.8.4/3.8.6.
|
|
14
|
+
|
|
15
|
+
## 0.2.0
|
|
16
|
+
|
|
17
|
+
Первый публичный релиз в виде отдельного пакета (ранее код жил внутри MaxGiveawayBot).
|
|
18
|
+
|
|
19
|
+
- `Bot`: `get_me`, `get_updates`, `send_message`, `edit_message`, `delete_message`, `get_message`
|
|
20
|
+
- закрепление: `pin_chat_message`, `pin_from_response`, `unpin_chat_message`
|
|
21
|
+
- чаты: `get_chat_by_id`, `get_chat_member`, `get_chat_members_batch`, `get_list_admin_chat`
|
|
22
|
+
- callback: `send_callback`
|
|
23
|
+
- медиа: `upload_file`, `create_media_attachment`, `send_message_with_media`, `edit_message_with_media`
|
|
24
|
+
- утилиты: `extract_message_id`, `build_link_button_attachment`
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: maxapi-lite
|
|
3
|
+
Version: 0.3.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
|
+
[](https://pypi.org/project/maxapi-lite/)
|
|
39
|
+
[](https://pypi.org/project/maxapi-lite/)
|
|
40
|
+
[](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=-1, # Демонстрационный ID — замените на ID своего чата
|
|
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=-1, 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
|
+
Можно передать `types=["message_created", "message_callback"]`. Библиотека оставляет
|
|
105
|
+
HTTP-таймауту запас не менее 10 секунд относительно `timeout` поллинга; явно
|
|
106
|
+
отключённые ограничения HTTP не включаются обратно. Допустимый `timeout` — 0–90,
|
|
107
|
+
`limit` — 1–1000. MAX рекомендует использовать Webhook для production: Long Polling
|
|
108
|
+
ограничен скоростью и сроком хранения событий ([документация](https://dev.max.ru/docs-api/methods/GET/updates)).
|
|
109
|
+
|
|
110
|
+
## Подключение и TLS
|
|
111
|
+
|
|
112
|
+
По умолчанию запросы идут на `https://platform-api2.max.ru`. Для другого сервера
|
|
113
|
+
передайте `base_url`. Настройки доступны как в `Bot`, так и в `MaxApiClient`:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
import ssl
|
|
117
|
+
import aiohttp
|
|
118
|
+
from maxapi_lite import Bot
|
|
119
|
+
|
|
120
|
+
tls = ssl.create_default_context()
|
|
121
|
+
# При необходимости добавьте доверенный сертификат в окружении приложения:
|
|
122
|
+
# tls.load_verify_locations(cafile="/path/to/trusted-ca.pem")
|
|
123
|
+
|
|
124
|
+
bot = Bot(
|
|
125
|
+
ssl_context=tls, # токен берётся из MAX_BOT_TOKEN
|
|
126
|
+
timeout=aiohttp.ClientTimeout(total=60),
|
|
127
|
+
transfer_timeout=aiohttp.ClientTimeout(
|
|
128
|
+
total=None, connect=30, sock_connect=30, sock_read=120,
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
API по умолчанию использует `total=300`. У upload/download нет общего ограничения
|
|
134
|
+
времени, но по умолчанию установлены таймауты соединения 30 с и чтения 120 с.
|
|
135
|
+
Вместо `ssl_context` можно передать `connector=aiohttp.TCPConnector(ssl=tls)`;
|
|
136
|
+
одновременно эти параметры передавать нельзя. Внешний connector закрывает
|
|
137
|
+
вызывающий код. Собственные сессии и connector библиотека закрывает через
|
|
138
|
+
`await bot.close_session()`; вызывайте его в `finally`.
|
|
139
|
+
|
|
140
|
+
## Входящие файлы и видео
|
|
141
|
+
|
|
142
|
+
Извлекайте URL из входящего события и передавайте его в `download_file`:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from maxapi_lite import Bot
|
|
146
|
+
|
|
147
|
+
async def save_incoming_video(bot: Bot, update: dict, destination: str) -> int:
|
|
148
|
+
for attachment in bot.extract_attachments(update):
|
|
149
|
+
if attachment.get("type") == "video":
|
|
150
|
+
url = (attachment.get("payload") or {}).get("url")
|
|
151
|
+
if url:
|
|
152
|
+
return await bot.download_file(url, destination)
|
|
153
|
+
return 0
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`download_file(url, dest_path, *, chunk_size=1<<20, max_retries=3)` возвращает число
|
|
157
|
+
сохранённых байтов. Скачивание идёт чанками через отдельную сессию **без токена
|
|
158
|
+
бота**, включая переходы по редиректам. `max_retries=3` — до трёх повторов после
|
|
159
|
+
первой попытки. При обрыве выполняется докачка через Range/If-Range, если сервер
|
|
160
|
+
предоставил сильный ETag или Last-Modified; без валидатора скачивание начинается
|
|
161
|
+
заново. Ответ 200 вместо 206 также перезаписывает временный файл с начала.
|
|
162
|
+
Размер сверяется с Content-Length/Content-Range, когда они доступны.
|
|
163
|
+
|
|
164
|
+
Существующий файл назначения заменяется атомарно после успешного скачивания.
|
|
165
|
+
При ошибке или отмене временный файл удаляется; между вызовами докачка не
|
|
166
|
+
сохраняется. Родительский каталог назначения должен существовать.
|
|
167
|
+
|
|
168
|
+
Для скачивания по готовому `payload.url` токен видео не нужен.
|
|
169
|
+
`await bot.get_video(video_token)` отдельно получает метаданные через API;
|
|
170
|
+
`urls` в ответе может быть `None`.
|
|
171
|
+
|
|
172
|
+
По наблюдениям, приведённым в задании ClipWizard: входящее `video` перекодируется
|
|
173
|
+
MAX и URL живёт около 24 часов; `file` сохраняет исходные байты и URL живёт около
|
|
174
|
+
30 дней; изображение может возвращаться в webp. Это наблюдения, а не гарантии
|
|
175
|
+
SDK: скачивайте входящие файлы своевременно. Ошибка HTTP 403 возвращается без
|
|
176
|
+
автоматических повторов; приложение может запросить свежую ссылку через MAX API,
|
|
177
|
+
если такая возможность доступна для вложения.
|
|
178
|
+
|
|
179
|
+
## Загрузка и отправка результата
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
await bot.send_message_with_media(
|
|
183
|
+
chat_id=-1, # замените на свой ID
|
|
184
|
+
media_type="video",
|
|
185
|
+
file_path="result.mp4",
|
|
186
|
+
text="Видео готово",
|
|
187
|
+
wait_attachments=True,
|
|
188
|
+
attachment_wait_timeout=60,
|
|
189
|
+
)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Файл передаётся потоком в multipart-поле `data`. Для video/audio библиотека
|
|
193
|
+
сохраняет токен из ответа `/uploads`, если сервер заливки не вернул собственный.
|
|
194
|
+
Токен вложения используется при отправке сообщения; передавать его вручную при
|
|
195
|
+
загрузке локального файла не требуется.
|
|
196
|
+
|
|
197
|
+
При `wait_attachments=True` отправка или редактирование повторяется только после
|
|
198
|
+
ошибки `attachment.not.ready`. Паузы растут 1, 2, 4, 8, 16 с, затем остаются 16 с;
|
|
199
|
+
запросы и паузы ограничены общим бюджетом `attachment_wait_timeout` (60 с по
|
|
200
|
+
умолчанию). Эти параметры работают у `send_message`, `edit_message` и обоих
|
|
201
|
+
методов `*_with_media`. По умолчанию ожидание выключено. Истечение бюджета после
|
|
202
|
+
определённого ответа not.ready даёт `AttachmentNotReady`; таймаут запроса с
|
|
203
|
+
неизвестным результатом остаётся `asyncio.TimeoutError`.
|
|
204
|
+
|
|
205
|
+
Размер локального файла проверяется до запроса `/uploads`:
|
|
206
|
+
|
|
207
|
+
| Тип | Максимум в байтах |
|
|
208
|
+
| --- | ---: |
|
|
209
|
+
| image | 50 000 000 |
|
|
210
|
+
| video | 250 000 000 |
|
|
211
|
+
| audio | 256 000 000 |
|
|
212
|
+
| file | 4 000 000 000 |
|
|
213
|
+
|
|
214
|
+
Используются консервативные десятичные МБ/ГБ. При превышении возникает
|
|
215
|
+
`FileTooLarge` с полями `size` и `limit`. MAX также ограничивает изображения
|
|
216
|
+
7680×7680 px и аудио 60 минутами; поддерживает MP4/MOV/MKV/WEBM для video.
|
|
217
|
+
Проверка содержимого, разрешения и длительности выполняется сервером; локально
|
|
218
|
+
библиотека не декодирует медиа ([лимиты MAX](https://dev.max.ru/docs-api/methods/POST/uploads)).
|
|
219
|
+
Resumable upload пока не реализован; после обрыва заливку нужно запускать заново.
|
|
220
|
+
|
|
221
|
+
## Клавиатуры и callback
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from maxapi_lite import CallbackButton, LinkButton
|
|
225
|
+
|
|
226
|
+
keyboard = bot.build_keyboard([
|
|
227
|
+
[CallbackButton(text="Обработать", payload="process")],
|
|
228
|
+
[LinkButton(text="Открыть сайт", url="https://example.com")],
|
|
229
|
+
])
|
|
230
|
+
await bot.send_message(chat_id=-1, text="Выберите действие", attachments=[keyboard])
|
|
231
|
+
await bot.send_callback(
|
|
232
|
+
callback_id="CALLBACK_ID_FROM_UPDATE",
|
|
233
|
+
notification="Готово",
|
|
234
|
+
message={"text": "Обработка запущена", "attachments": []},
|
|
235
|
+
)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Модели кнопок сериализуются с обязательным полем `type`. `build_keyboard`
|
|
239
|
+
принимает ряды моделей или словарей и возвращает одно вложение inline_keyboard.
|
|
240
|
+
`send_callback` может отправлять уведомление, обновлённое сообщение или оба.
|
|
241
|
+
|
|
242
|
+
## Ошибки и повторы
|
|
243
|
+
|
|
244
|
+
`MaxApiError.code` — числовой HTTP status, `MaxApiError.api_code` — строковый код
|
|
245
|
+
ошибки MAX, если он есть. `FileTooLarge`, `AttachmentNotReady`, `RateLimited`
|
|
246
|
+
наследуют `MaxApiError`; `InvalidToken` сохраняет прежнее поведение.
|
|
247
|
+
|
|
248
|
+
При HTTP 429 возникает `RateLimited`; поле `retry_after` содержит задержку в
|
|
249
|
+
секундах из Retry-After либо `None`. 429 автоматически не повторяется. MAX
|
|
250
|
+
указывает общий предел 30 rps, а для `/answers` — не более двух ответов в секунду
|
|
251
|
+
в один диалог/чат/канал; очередь и регулирование частоты остаются в приложении
|
|
252
|
+
([uploads](https://dev.max.ru/docs-api/methods/POST/uploads),
|
|
253
|
+
[answers](https://dev.max.ru/docs-api/methods/POST/answers)).
|
|
254
|
+
|
|
255
|
+
Сетевые ошибки отправки сообщений автоматически не повторяются: после обрыва
|
|
256
|
+
сервер уже мог принять сообщение, и повтор способен создать дубликат.
|
|
257
|
+
|
|
258
|
+
## Методы `Bot`
|
|
259
|
+
|
|
260
|
+
| Группа | Методы |
|
|
261
|
+
| --- | --- |
|
|
262
|
+
| Бот и обновления | `get_me`, `get_updates`, `extract_attachments` |
|
|
263
|
+
| Сообщения | `send_message`, `edit_message`, `delete_message`, `get_message` |
|
|
264
|
+
| Медиа | `upload_file`, `download_file`, `get_video`, `create_media_attachment`, `send_message_with_media`, `edit_message_with_media` |
|
|
265
|
+
| Закрепление | `pin_chat_message`, `pin_from_response`, `unpin_chat_message` |
|
|
266
|
+
| Чаты | `get_chat_by_id`, `get_chat_member`, `get_chat_members_batch`, `get_list_admin_chat` |
|
|
267
|
+
| Callback | `send_callback` |
|
|
268
|
+
| Утилиты | `extract_message_id`, `build_keyboard`, `build_link_button_attachment` |
|
|
269
|
+
|
|
270
|
+
Методы возвращают «сырые» `dict` из ответа API. Ошибки API поднимаются как `MaxApiError`
|
|
271
|
+
(с полем `code`), ошибка авторизации — как `InvalidToken`.
|
|
272
|
+
|
|
273
|
+
## Поддерживаемые `media_type`
|
|
274
|
+
|
|
275
|
+
- `photo`, `animation`, `image` → загрузка как `image`
|
|
276
|
+
- `video`
|
|
277
|
+
- `audio`
|
|
278
|
+
- `document`, `file`
|
|
279
|
+
|
|
280
|
+
## Примечания
|
|
281
|
+
|
|
282
|
+
- `chat_id` / `user_id` передаются query-параметрами, как требует MAX API.
|
|
283
|
+
- `edit_message(..., attachments=[])` явно очищает вложения; `attachments=None` оставляет их как есть.
|
|
284
|
+
- Формат (`parse_mode`) применяется к редактированию только если передан `text`.
|
|
285
|
+
- Для upload используется multipart-поле `data`.
|
|
286
|
+
|
|
287
|
+
## Разработка
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
pip install -e ".[test]"
|
|
291
|
+
pytest
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Релиз: поднять `__version__` в `src/maxapi_lite/__init__.py`, дополнить `CHANGELOG.md`,
|
|
295
|
+
создать GitHub Release с тегом `vX.Y.Z` — workflow `publish.yml` соберёт пакет и опубликует его на PyPI.
|
|
296
|
+
|
|
297
|
+
## Лицензия
|
|
298
|
+
|
|
299
|
+
MIT
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# maxapi-lite
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/maxapi-lite/)
|
|
4
|
+
[](https://pypi.org/project/maxapi-lite/)
|
|
5
|
+
[](https://github.com/kiriyk/maxapi-lite/actions/workflows/ci.yml)
|
|
6
|
+
|
|
7
|
+
Легковесный асинхронный клиент [MAX Bot API](https://dev.max.ru/docs-api), совместимый с `aiohttp 3.8.x`.
|
|
8
|
+
|
|
9
|
+
Пакет нужен, когда в проекте уже зафиксирован старый `aiohttp` — например, рядом живёт
|
|
10
|
+
`aiogram 2.x` (`aiohttp<3.9`), а официальный клиент `maxapi` требует `aiohttp>=3.12`.
|
|
11
|
+
Зависимости — только `aiohttp>=3.8` и `pydantic>=2`.
|
|
12
|
+
|
|
13
|
+
*Lightweight async client for the MAX messenger Bot API that works with aiohttp 3.8,
|
|
14
|
+
so it can share an environment with aiogram 2.x.*
|
|
15
|
+
|
|
16
|
+
## Установка
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install maxapi-lite
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Быстрый старт
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import asyncio
|
|
26
|
+
from maxapi_lite import Bot, ParseMode
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def main():
|
|
30
|
+
bot = Bot(token="YOUR_MAX_TOKEN", parse_mode=ParseMode.HTML) # или переменная окружения MAX_BOT_TOKEN
|
|
31
|
+
|
|
32
|
+
# Кнопка-ссылка
|
|
33
|
+
kb_attachment = bot.build_link_button_attachment(
|
|
34
|
+
text="Открыть MiniApp",
|
|
35
|
+
url="https://example.com/app?startapp=123",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Отправка фото + кнопки
|
|
39
|
+
result = await bot.send_message_with_media(
|
|
40
|
+
chat_id=-1, # Демонстрационный ID — замените на ID своего чата
|
|
41
|
+
text="<b>Розыгрыш</b>",
|
|
42
|
+
media_type="photo",
|
|
43
|
+
file_path="banner.jpg", # или media_url="https://..."
|
|
44
|
+
attachments=[kb_attachment],
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Извлечение mid и закрепление
|
|
48
|
+
mid = bot.extract_message_id(result)
|
|
49
|
+
if mid:
|
|
50
|
+
await bot.pin_chat_message(chat_id=-1, message_id=mid)
|
|
51
|
+
|
|
52
|
+
await bot.close_session()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
asyncio.run(main())
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Long polling
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
marker = None
|
|
62
|
+
while True:
|
|
63
|
+
data = await bot.get_updates(timeout=30, marker=marker)
|
|
64
|
+
for update in data.get("updates", []):
|
|
65
|
+
...
|
|
66
|
+
marker = data.get("marker", marker)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Можно передать `types=["message_created", "message_callback"]`. Библиотека оставляет
|
|
70
|
+
HTTP-таймауту запас не менее 10 секунд относительно `timeout` поллинга; явно
|
|
71
|
+
отключённые ограничения HTTP не включаются обратно. Допустимый `timeout` — 0–90,
|
|
72
|
+
`limit` — 1–1000. MAX рекомендует использовать Webhook для production: Long Polling
|
|
73
|
+
ограничен скоростью и сроком хранения событий ([документация](https://dev.max.ru/docs-api/methods/GET/updates)).
|
|
74
|
+
|
|
75
|
+
## Подключение и TLS
|
|
76
|
+
|
|
77
|
+
По умолчанию запросы идут на `https://platform-api2.max.ru`. Для другого сервера
|
|
78
|
+
передайте `base_url`. Настройки доступны как в `Bot`, так и в `MaxApiClient`:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import ssl
|
|
82
|
+
import aiohttp
|
|
83
|
+
from maxapi_lite import Bot
|
|
84
|
+
|
|
85
|
+
tls = ssl.create_default_context()
|
|
86
|
+
# При необходимости добавьте доверенный сертификат в окружении приложения:
|
|
87
|
+
# tls.load_verify_locations(cafile="/path/to/trusted-ca.pem")
|
|
88
|
+
|
|
89
|
+
bot = Bot(
|
|
90
|
+
ssl_context=tls, # токен берётся из MAX_BOT_TOKEN
|
|
91
|
+
timeout=aiohttp.ClientTimeout(total=60),
|
|
92
|
+
transfer_timeout=aiohttp.ClientTimeout(
|
|
93
|
+
total=None, connect=30, sock_connect=30, sock_read=120,
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
API по умолчанию использует `total=300`. У upload/download нет общего ограничения
|
|
99
|
+
времени, но по умолчанию установлены таймауты соединения 30 с и чтения 120 с.
|
|
100
|
+
Вместо `ssl_context` можно передать `connector=aiohttp.TCPConnector(ssl=tls)`;
|
|
101
|
+
одновременно эти параметры передавать нельзя. Внешний connector закрывает
|
|
102
|
+
вызывающий код. Собственные сессии и connector библиотека закрывает через
|
|
103
|
+
`await bot.close_session()`; вызывайте его в `finally`.
|
|
104
|
+
|
|
105
|
+
## Входящие файлы и видео
|
|
106
|
+
|
|
107
|
+
Извлекайте URL из входящего события и передавайте его в `download_file`:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from maxapi_lite import Bot
|
|
111
|
+
|
|
112
|
+
async def save_incoming_video(bot: Bot, update: dict, destination: str) -> int:
|
|
113
|
+
for attachment in bot.extract_attachments(update):
|
|
114
|
+
if attachment.get("type") == "video":
|
|
115
|
+
url = (attachment.get("payload") or {}).get("url")
|
|
116
|
+
if url:
|
|
117
|
+
return await bot.download_file(url, destination)
|
|
118
|
+
return 0
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`download_file(url, dest_path, *, chunk_size=1<<20, max_retries=3)` возвращает число
|
|
122
|
+
сохранённых байтов. Скачивание идёт чанками через отдельную сессию **без токена
|
|
123
|
+
бота**, включая переходы по редиректам. `max_retries=3` — до трёх повторов после
|
|
124
|
+
первой попытки. При обрыве выполняется докачка через Range/If-Range, если сервер
|
|
125
|
+
предоставил сильный ETag или Last-Modified; без валидатора скачивание начинается
|
|
126
|
+
заново. Ответ 200 вместо 206 также перезаписывает временный файл с начала.
|
|
127
|
+
Размер сверяется с Content-Length/Content-Range, когда они доступны.
|
|
128
|
+
|
|
129
|
+
Существующий файл назначения заменяется атомарно после успешного скачивания.
|
|
130
|
+
При ошибке или отмене временный файл удаляется; между вызовами докачка не
|
|
131
|
+
сохраняется. Родительский каталог назначения должен существовать.
|
|
132
|
+
|
|
133
|
+
Для скачивания по готовому `payload.url` токен видео не нужен.
|
|
134
|
+
`await bot.get_video(video_token)` отдельно получает метаданные через API;
|
|
135
|
+
`urls` в ответе может быть `None`.
|
|
136
|
+
|
|
137
|
+
По наблюдениям, приведённым в задании ClipWizard: входящее `video` перекодируется
|
|
138
|
+
MAX и URL живёт около 24 часов; `file` сохраняет исходные байты и URL живёт около
|
|
139
|
+
30 дней; изображение может возвращаться в webp. Это наблюдения, а не гарантии
|
|
140
|
+
SDK: скачивайте входящие файлы своевременно. Ошибка HTTP 403 возвращается без
|
|
141
|
+
автоматических повторов; приложение может запросить свежую ссылку через MAX API,
|
|
142
|
+
если такая возможность доступна для вложения.
|
|
143
|
+
|
|
144
|
+
## Загрузка и отправка результата
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
await bot.send_message_with_media(
|
|
148
|
+
chat_id=-1, # замените на свой ID
|
|
149
|
+
media_type="video",
|
|
150
|
+
file_path="result.mp4",
|
|
151
|
+
text="Видео готово",
|
|
152
|
+
wait_attachments=True,
|
|
153
|
+
attachment_wait_timeout=60,
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Файл передаётся потоком в multipart-поле `data`. Для video/audio библиотека
|
|
158
|
+
сохраняет токен из ответа `/uploads`, если сервер заливки не вернул собственный.
|
|
159
|
+
Токен вложения используется при отправке сообщения; передавать его вручную при
|
|
160
|
+
загрузке локального файла не требуется.
|
|
161
|
+
|
|
162
|
+
При `wait_attachments=True` отправка или редактирование повторяется только после
|
|
163
|
+
ошибки `attachment.not.ready`. Паузы растут 1, 2, 4, 8, 16 с, затем остаются 16 с;
|
|
164
|
+
запросы и паузы ограничены общим бюджетом `attachment_wait_timeout` (60 с по
|
|
165
|
+
умолчанию). Эти параметры работают у `send_message`, `edit_message` и обоих
|
|
166
|
+
методов `*_with_media`. По умолчанию ожидание выключено. Истечение бюджета после
|
|
167
|
+
определённого ответа not.ready даёт `AttachmentNotReady`; таймаут запроса с
|
|
168
|
+
неизвестным результатом остаётся `asyncio.TimeoutError`.
|
|
169
|
+
|
|
170
|
+
Размер локального файла проверяется до запроса `/uploads`:
|
|
171
|
+
|
|
172
|
+
| Тип | Максимум в байтах |
|
|
173
|
+
| --- | ---: |
|
|
174
|
+
| image | 50 000 000 |
|
|
175
|
+
| video | 250 000 000 |
|
|
176
|
+
| audio | 256 000 000 |
|
|
177
|
+
| file | 4 000 000 000 |
|
|
178
|
+
|
|
179
|
+
Используются консервативные десятичные МБ/ГБ. При превышении возникает
|
|
180
|
+
`FileTooLarge` с полями `size` и `limit`. MAX также ограничивает изображения
|
|
181
|
+
7680×7680 px и аудио 60 минутами; поддерживает MP4/MOV/MKV/WEBM для video.
|
|
182
|
+
Проверка содержимого, разрешения и длительности выполняется сервером; локально
|
|
183
|
+
библиотека не декодирует медиа ([лимиты MAX](https://dev.max.ru/docs-api/methods/POST/uploads)).
|
|
184
|
+
Resumable upload пока не реализован; после обрыва заливку нужно запускать заново.
|
|
185
|
+
|
|
186
|
+
## Клавиатуры и callback
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from maxapi_lite import CallbackButton, LinkButton
|
|
190
|
+
|
|
191
|
+
keyboard = bot.build_keyboard([
|
|
192
|
+
[CallbackButton(text="Обработать", payload="process")],
|
|
193
|
+
[LinkButton(text="Открыть сайт", url="https://example.com")],
|
|
194
|
+
])
|
|
195
|
+
await bot.send_message(chat_id=-1, text="Выберите действие", attachments=[keyboard])
|
|
196
|
+
await bot.send_callback(
|
|
197
|
+
callback_id="CALLBACK_ID_FROM_UPDATE",
|
|
198
|
+
notification="Готово",
|
|
199
|
+
message={"text": "Обработка запущена", "attachments": []},
|
|
200
|
+
)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Модели кнопок сериализуются с обязательным полем `type`. `build_keyboard`
|
|
204
|
+
принимает ряды моделей или словарей и возвращает одно вложение inline_keyboard.
|
|
205
|
+
`send_callback` может отправлять уведомление, обновлённое сообщение или оба.
|
|
206
|
+
|
|
207
|
+
## Ошибки и повторы
|
|
208
|
+
|
|
209
|
+
`MaxApiError.code` — числовой HTTP status, `MaxApiError.api_code` — строковый код
|
|
210
|
+
ошибки MAX, если он есть. `FileTooLarge`, `AttachmentNotReady`, `RateLimited`
|
|
211
|
+
наследуют `MaxApiError`; `InvalidToken` сохраняет прежнее поведение.
|
|
212
|
+
|
|
213
|
+
При HTTP 429 возникает `RateLimited`; поле `retry_after` содержит задержку в
|
|
214
|
+
секундах из Retry-After либо `None`. 429 автоматически не повторяется. MAX
|
|
215
|
+
указывает общий предел 30 rps, а для `/answers` — не более двух ответов в секунду
|
|
216
|
+
в один диалог/чат/канал; очередь и регулирование частоты остаются в приложении
|
|
217
|
+
([uploads](https://dev.max.ru/docs-api/methods/POST/uploads),
|
|
218
|
+
[answers](https://dev.max.ru/docs-api/methods/POST/answers)).
|
|
219
|
+
|
|
220
|
+
Сетевые ошибки отправки сообщений автоматически не повторяются: после обрыва
|
|
221
|
+
сервер уже мог принять сообщение, и повтор способен создать дубликат.
|
|
222
|
+
|
|
223
|
+
## Методы `Bot`
|
|
224
|
+
|
|
225
|
+
| Группа | Методы |
|
|
226
|
+
| --- | --- |
|
|
227
|
+
| Бот и обновления | `get_me`, `get_updates`, `extract_attachments` |
|
|
228
|
+
| Сообщения | `send_message`, `edit_message`, `delete_message`, `get_message` |
|
|
229
|
+
| Медиа | `upload_file`, `download_file`, `get_video`, `create_media_attachment`, `send_message_with_media`, `edit_message_with_media` |
|
|
230
|
+
| Закрепление | `pin_chat_message`, `pin_from_response`, `unpin_chat_message` |
|
|
231
|
+
| Чаты | `get_chat_by_id`, `get_chat_member`, `get_chat_members_batch`, `get_list_admin_chat` |
|
|
232
|
+
| Callback | `send_callback` |
|
|
233
|
+
| Утилиты | `extract_message_id`, `build_keyboard`, `build_link_button_attachment` |
|
|
234
|
+
|
|
235
|
+
Методы возвращают «сырые» `dict` из ответа API. Ошибки API поднимаются как `MaxApiError`
|
|
236
|
+
(с полем `code`), ошибка авторизации — как `InvalidToken`.
|
|
237
|
+
|
|
238
|
+
## Поддерживаемые `media_type`
|
|
239
|
+
|
|
240
|
+
- `photo`, `animation`, `image` → загрузка как `image`
|
|
241
|
+
- `video`
|
|
242
|
+
- `audio`
|
|
243
|
+
- `document`, `file`
|
|
244
|
+
|
|
245
|
+
## Примечания
|
|
246
|
+
|
|
247
|
+
- `chat_id` / `user_id` передаются query-параметрами, как требует MAX API.
|
|
248
|
+
- `edit_message(..., attachments=[])` явно очищает вложения; `attachments=None` оставляет их как есть.
|
|
249
|
+
- Формат (`parse_mode`) применяется к редактированию только если передан `text`.
|
|
250
|
+
- Для upload используется multipart-поле `data`.
|
|
251
|
+
|
|
252
|
+
## Разработка
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
pip install -e ".[test]"
|
|
256
|
+
pytest
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Релиз: поднять `__version__` в `src/maxapi_lite/__init__.py`, дополнить `CHANGELOG.md`,
|
|
260
|
+
создать GitHub Release с тегом `vX.Y.Z` — workflow `publish.yml` соберёт пакет и опубликует его на PyPI.
|
|
261
|
+
|
|
262
|
+
## Лицензия
|
|
263
|
+
|
|
264
|
+
MIT
|
|
@@ -7,7 +7,7 @@ maxapi_lite - легковесный асинхронный клиент MAX Bot
|
|
|
7
7
|
|
|
8
8
|
from .bot import Bot
|
|
9
9
|
from .enums import ChatPermission, ParseMode
|
|
10
|
-
from .exceptions import InvalidToken, MaxApiError
|
|
10
|
+
from .exceptions import AttachmentNotReady, FileTooLarge, InvalidToken, MaxApiError, RateLimited
|
|
11
11
|
from .types import (
|
|
12
12
|
Attachment,
|
|
13
13
|
ButtonsPayload,
|
|
@@ -19,7 +19,7 @@ from .types import (
|
|
|
19
19
|
RequestGeoLocationButton,
|
|
20
20
|
)
|
|
21
21
|
|
|
22
|
-
__version__ = "0.
|
|
22
|
+
__version__ = "0.3.0"
|
|
23
23
|
|
|
24
24
|
__all__ = [
|
|
25
25
|
# Main class
|
|
@@ -39,4 +39,7 @@ __all__ = [
|
|
|
39
39
|
# Exceptions
|
|
40
40
|
"MaxApiError",
|
|
41
41
|
"InvalidToken",
|
|
42
|
+
"FileTooLarge",
|
|
43
|
+
"AttachmentNotReady",
|
|
44
|
+
"RateLimited",
|
|
42
45
|
]
|