pykodaribot 0.1.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.
- pykodaribot-0.1.0/PKG-INFO +134 -0
- pykodaribot-0.1.0/README.md +119 -0
- pykodaribot-0.1.0/pykodaribot/__init__.py +31 -0
- pykodaribot-0.1.0/pykodaribot/bot.py +208 -0
- pykodaribot-0.1.0/pykodaribot/dispatcher.py +78 -0
- pykodaribot-0.1.0/pykodaribot/filters.py +61 -0
- pykodaribot-0.1.0/pykodaribot/types.py +138 -0
- pykodaribot-0.1.0/pykodaribot.egg-info/PKG-INFO +134 -0
- pykodaribot-0.1.0/pykodaribot.egg-info/SOURCES.txt +12 -0
- pykodaribot-0.1.0/pykodaribot.egg-info/dependency_links.txt +1 -0
- pykodaribot-0.1.0/pykodaribot.egg-info/requires.txt +1 -0
- pykodaribot-0.1.0/pykodaribot.egg-info/top_level.txt +1 -0
- pykodaribot-0.1.0/pyproject.toml +26 -0
- pykodaribot-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pykodaribot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK для создания ботов в Kodari Messenger
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://msg.kodari.ru
|
|
7
|
+
Project-URL: Documentation, https://docs.kodari.ru/
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Framework :: AsyncIO
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: aiohttp>=3.9
|
|
15
|
+
|
|
16
|
+
# pykodaribot
|
|
17
|
+
|
|
18
|
+
Python SDK для создания ботов в [Kodari Messenger](https://msg.kodari.ru).
|
|
19
|
+
|
|
20
|
+
**Документация:** [docs.kodari.ru](https://docs.kodari.ru/)
|
|
21
|
+
|
|
22
|
+
## Установка
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install pykodaribot
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Требования:** Python 3.9+
|
|
29
|
+
|
|
30
|
+
## Быстрый старт
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import asyncio
|
|
34
|
+
from pykodaribot import Bot, Dispatcher, filters
|
|
35
|
+
|
|
36
|
+
bot = Bot("bot_ВАШ_ТОКЕН")
|
|
37
|
+
dp = Dispatcher()
|
|
38
|
+
|
|
39
|
+
@dp.message(filters.command("start"))
|
|
40
|
+
async def start(msg):
|
|
41
|
+
await msg.answer("Привет!")
|
|
42
|
+
|
|
43
|
+
@dp.message()
|
|
44
|
+
async def echo(msg):
|
|
45
|
+
if msg.text:
|
|
46
|
+
await msg.reply(msg.text)
|
|
47
|
+
|
|
48
|
+
asyncio.run(dp.start_polling(bot))
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Токен
|
|
52
|
+
|
|
53
|
+
Напишите @BotFather в мессенджере, введите `/newbot` и следуйте инструкциям.
|
|
54
|
+
|
|
55
|
+
## API
|
|
56
|
+
|
|
57
|
+
### Bot
|
|
58
|
+
|
|
59
|
+
| Метод | Описание |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `get_me()` | Информация о боте |
|
|
62
|
+
| `send_message(chat_id, text, reply_to_message_id, reply_markup)` | Отправить текст |
|
|
63
|
+
| `send_photo(chat_id, media, caption, reply_to_message_id)` | Отправить фото |
|
|
64
|
+
| `send_document(chat_id, media, caption, reply_to_message_id)` | Отправить файл |
|
|
65
|
+
| `send_video(chat_id, media, caption, reply_to_message_id)` | Отправить видео |
|
|
66
|
+
| `send_audio(chat_id, media, caption, reply_to_message_id)` | Отправить аудио |
|
|
67
|
+
| `edit_message_text(chat_id, message_id, text, reply_markup)` | Редактировать сообщение |
|
|
68
|
+
| `delete_message(chat_id, message_id)` | Удалить сообщение |
|
|
69
|
+
| `get_updates(offset, timeout, limit)` | Получить апдейты |
|
|
70
|
+
| `answer_callback_query(callback_query_id)` | Ответить на callback |
|
|
71
|
+
| `set_my_commands(commands)` | Установить команды меню |
|
|
72
|
+
| `get_my_commands()` | Получить команды меню |
|
|
73
|
+
| `set_my_name(name)` | Изменить имя бота |
|
|
74
|
+
| `set_my_description(description)` | Изменить описание |
|
|
75
|
+
| `set_my_short_description(short_description)` | Изменить краткое описание |
|
|
76
|
+
| `get_chat(chat_id)` | Информация о чате |
|
|
77
|
+
|
|
78
|
+
### Dispatcher
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
dp = Dispatcher()
|
|
82
|
+
|
|
83
|
+
@dp.message(*filters) # новое сообщение
|
|
84
|
+
@dp.edited_message(*filters) # изменённое сообщение
|
|
85
|
+
@dp.callback_query(*filters) # нажатие inline-кнопки
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Фильтры
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from pykodaribot import filters
|
|
92
|
+
|
|
93
|
+
filters.command("start", "help") # команды /start или /help
|
|
94
|
+
filters.text(r"привет|hello") # regex по тексту
|
|
95
|
+
filters.callback_data("yes") # точное совпадение callback_data
|
|
96
|
+
filters.callback_data_prefix("action:") # callback_data начинается с префикса
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Inline-клавиатура
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from pykodaribot import InlineKeyboardMarkup, InlineKeyboardButton
|
|
103
|
+
|
|
104
|
+
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
|
105
|
+
[
|
|
106
|
+
InlineKeyboardButton(text="Да", callback_data="yes"),
|
|
107
|
+
InlineKeyboardButton(text="Нет", callback_data="no"),
|
|
108
|
+
],
|
|
109
|
+
[
|
|
110
|
+
InlineKeyboardButton(text="Открыть сайт", url="https://kodari.ru"),
|
|
111
|
+
],
|
|
112
|
+
])
|
|
113
|
+
|
|
114
|
+
await msg.answer("Выберите:", reply_markup=keyboard)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Отправка медиа
|
|
118
|
+
|
|
119
|
+
`media` принимает путь к файлу, `Path`, `bytes` или file-like объект:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
await bot.send_photo(chat_id, "photo.jpg", caption="Смотри!")
|
|
123
|
+
await bot.send_photo(chat_id, Path("/tmp/image.png"))
|
|
124
|
+
await bot.send_document(chat_id, open("report.pdf", "rb"))
|
|
125
|
+
|
|
126
|
+
with open("clip.mp4", "rb") as f:
|
|
127
|
+
await bot.send_video(chat_id, f, caption="Видео")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Примеры
|
|
131
|
+
|
|
132
|
+
- [`examples/echo_bot.py`](examples/echo_bot.py) — эхо-бот
|
|
133
|
+
- [`examples/inline_bot.py`](examples/inline_bot.py) — inline-кнопки и счётчик
|
|
134
|
+
- [`examples/commands_bot.py`](examples/commands_bot.py) — команды меню
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# pykodaribot
|
|
2
|
+
|
|
3
|
+
Python SDK для создания ботов в [Kodari Messenger](https://msg.kodari.ru).
|
|
4
|
+
|
|
5
|
+
**Документация:** [docs.kodari.ru](https://docs.kodari.ru/)
|
|
6
|
+
|
|
7
|
+
## Установка
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install pykodaribot
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**Требования:** Python 3.9+
|
|
14
|
+
|
|
15
|
+
## Быстрый старт
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import asyncio
|
|
19
|
+
from pykodaribot import Bot, Dispatcher, filters
|
|
20
|
+
|
|
21
|
+
bot = Bot("bot_ВАШ_ТОКЕН")
|
|
22
|
+
dp = Dispatcher()
|
|
23
|
+
|
|
24
|
+
@dp.message(filters.command("start"))
|
|
25
|
+
async def start(msg):
|
|
26
|
+
await msg.answer("Привет!")
|
|
27
|
+
|
|
28
|
+
@dp.message()
|
|
29
|
+
async def echo(msg):
|
|
30
|
+
if msg.text:
|
|
31
|
+
await msg.reply(msg.text)
|
|
32
|
+
|
|
33
|
+
asyncio.run(dp.start_polling(bot))
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Токен
|
|
37
|
+
|
|
38
|
+
Напишите @BotFather в мессенджере, введите `/newbot` и следуйте инструкциям.
|
|
39
|
+
|
|
40
|
+
## API
|
|
41
|
+
|
|
42
|
+
### Bot
|
|
43
|
+
|
|
44
|
+
| Метод | Описание |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `get_me()` | Информация о боте |
|
|
47
|
+
| `send_message(chat_id, text, reply_to_message_id, reply_markup)` | Отправить текст |
|
|
48
|
+
| `send_photo(chat_id, media, caption, reply_to_message_id)` | Отправить фото |
|
|
49
|
+
| `send_document(chat_id, media, caption, reply_to_message_id)` | Отправить файл |
|
|
50
|
+
| `send_video(chat_id, media, caption, reply_to_message_id)` | Отправить видео |
|
|
51
|
+
| `send_audio(chat_id, media, caption, reply_to_message_id)` | Отправить аудио |
|
|
52
|
+
| `edit_message_text(chat_id, message_id, text, reply_markup)` | Редактировать сообщение |
|
|
53
|
+
| `delete_message(chat_id, message_id)` | Удалить сообщение |
|
|
54
|
+
| `get_updates(offset, timeout, limit)` | Получить апдейты |
|
|
55
|
+
| `answer_callback_query(callback_query_id)` | Ответить на callback |
|
|
56
|
+
| `set_my_commands(commands)` | Установить команды меню |
|
|
57
|
+
| `get_my_commands()` | Получить команды меню |
|
|
58
|
+
| `set_my_name(name)` | Изменить имя бота |
|
|
59
|
+
| `set_my_description(description)` | Изменить описание |
|
|
60
|
+
| `set_my_short_description(short_description)` | Изменить краткое описание |
|
|
61
|
+
| `get_chat(chat_id)` | Информация о чате |
|
|
62
|
+
|
|
63
|
+
### Dispatcher
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
dp = Dispatcher()
|
|
67
|
+
|
|
68
|
+
@dp.message(*filters) # новое сообщение
|
|
69
|
+
@dp.edited_message(*filters) # изменённое сообщение
|
|
70
|
+
@dp.callback_query(*filters) # нажатие inline-кнопки
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Фильтры
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from pykodaribot import filters
|
|
77
|
+
|
|
78
|
+
filters.command("start", "help") # команды /start или /help
|
|
79
|
+
filters.text(r"привет|hello") # regex по тексту
|
|
80
|
+
filters.callback_data("yes") # точное совпадение callback_data
|
|
81
|
+
filters.callback_data_prefix("action:") # callback_data начинается с префикса
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Inline-клавиатура
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from pykodaribot import InlineKeyboardMarkup, InlineKeyboardButton
|
|
88
|
+
|
|
89
|
+
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
|
90
|
+
[
|
|
91
|
+
InlineKeyboardButton(text="Да", callback_data="yes"),
|
|
92
|
+
InlineKeyboardButton(text="Нет", callback_data="no"),
|
|
93
|
+
],
|
|
94
|
+
[
|
|
95
|
+
InlineKeyboardButton(text="Открыть сайт", url="https://kodari.ru"),
|
|
96
|
+
],
|
|
97
|
+
])
|
|
98
|
+
|
|
99
|
+
await msg.answer("Выберите:", reply_markup=keyboard)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Отправка медиа
|
|
103
|
+
|
|
104
|
+
`media` принимает путь к файлу, `Path`, `bytes` или file-like объект:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
await bot.send_photo(chat_id, "photo.jpg", caption="Смотри!")
|
|
108
|
+
await bot.send_photo(chat_id, Path("/tmp/image.png"))
|
|
109
|
+
await bot.send_document(chat_id, open("report.pdf", "rb"))
|
|
110
|
+
|
|
111
|
+
with open("clip.mp4", "rb") as f:
|
|
112
|
+
await bot.send_video(chat_id, f, caption="Видео")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Примеры
|
|
116
|
+
|
|
117
|
+
- [`examples/echo_bot.py`](examples/echo_bot.py) — эхо-бот
|
|
118
|
+
- [`examples/inline_bot.py`](examples/inline_bot.py) — inline-кнопки и счётчик
|
|
119
|
+
- [`examples/commands_bot.py`](examples/commands_bot.py) — команды меню
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from .bot import Bot, BotError
|
|
2
|
+
from .dispatcher import Dispatcher, Router
|
|
3
|
+
from .types import (
|
|
4
|
+
BotCommand,
|
|
5
|
+
CallbackQuery,
|
|
6
|
+
Chat,
|
|
7
|
+
InlineKeyboardButton,
|
|
8
|
+
InlineKeyboardMarkup,
|
|
9
|
+
Message,
|
|
10
|
+
Update,
|
|
11
|
+
User,
|
|
12
|
+
)
|
|
13
|
+
from . import filters
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Bot",
|
|
19
|
+
"BotError",
|
|
20
|
+
"Dispatcher",
|
|
21
|
+
"Router",
|
|
22
|
+
"User",
|
|
23
|
+
"Chat",
|
|
24
|
+
"Message",
|
|
25
|
+
"CallbackQuery",
|
|
26
|
+
"Update",
|
|
27
|
+
"InlineKeyboardMarkup",
|
|
28
|
+
"InlineKeyboardButton",
|
|
29
|
+
"BotCommand",
|
|
30
|
+
"filters",
|
|
31
|
+
]
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import BinaryIO, List, Optional, Union
|
|
4
|
+
|
|
5
|
+
import aiohttp
|
|
6
|
+
|
|
7
|
+
from .types import (
|
|
8
|
+
BotCommand, Chat, InlineKeyboardMarkup,
|
|
9
|
+
Message, Update, User,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
MediaInput = Union[str, bytes, Path, BinaryIO]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BotError(Exception):
|
|
16
|
+
def __init__(self, error_code: int, description: str):
|
|
17
|
+
self.error_code = error_code
|
|
18
|
+
self.description = description
|
|
19
|
+
super().__init__(f"[{error_code}] {description}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Bot:
|
|
23
|
+
BASE_URL = "https://api.kodari.ru/msg/bot.php"
|
|
24
|
+
|
|
25
|
+
def __init__(self, token: str):
|
|
26
|
+
self._token = token
|
|
27
|
+
self._headers = {"Authorization": f"Bearer {token}"}
|
|
28
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
29
|
+
|
|
30
|
+
async def _get_session(self) -> aiohttp.ClientSession:
|
|
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) -> None:
|
|
36
|
+
if self._session and not self._session.closed:
|
|
37
|
+
await self._session.close()
|
|
38
|
+
|
|
39
|
+
async def __aenter__(self) -> "Bot":
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
async def __aexit__(self, *_) -> None:
|
|
43
|
+
await self.close()
|
|
44
|
+
|
|
45
|
+
async def _get(self, method: str, **params) -> object:
|
|
46
|
+
session = await self._get_session()
|
|
47
|
+
params["method"] = method
|
|
48
|
+
async with session.get(self.BASE_URL, params=params) as r:
|
|
49
|
+
data = await r.json()
|
|
50
|
+
if not data.get("ok"):
|
|
51
|
+
raise BotError(data.get("error_code", 0), data.get("description", "Unknown"))
|
|
52
|
+
return data["result"]
|
|
53
|
+
|
|
54
|
+
async def _post(self, method: str, **body) -> object:
|
|
55
|
+
session = await self._get_session()
|
|
56
|
+
url = f"{self.BASE_URL}?method={method}"
|
|
57
|
+
body = {k: v for k, v in body.items() if v is not None}
|
|
58
|
+
async with session.post(url, json=body) as r:
|
|
59
|
+
data = await r.json()
|
|
60
|
+
if not data.get("ok"):
|
|
61
|
+
raise BotError(data.get("error_code", 0), data.get("description", "Unknown"))
|
|
62
|
+
return data["result"]
|
|
63
|
+
|
|
64
|
+
async def _post_media(self, method: str, chat_id: int, media: MediaInput,
|
|
65
|
+
caption: Optional[str], reply_to_message_id: Optional[int]) -> object:
|
|
66
|
+
session = await self._get_session()
|
|
67
|
+
url = f"{self.BASE_URL}?method={method}"
|
|
68
|
+
form = aiohttp.FormData()
|
|
69
|
+
form.add_field("chat_id", str(chat_id))
|
|
70
|
+
if caption is not None:
|
|
71
|
+
form.add_field("caption", caption)
|
|
72
|
+
if reply_to_message_id is not None:
|
|
73
|
+
form.add_field("reply_to_message_id", str(reply_to_message_id))
|
|
74
|
+
if isinstance(media, (str, Path)):
|
|
75
|
+
path = Path(media)
|
|
76
|
+
with open(path, "rb") as f:
|
|
77
|
+
form.add_field("media", f, filename=path.name)
|
|
78
|
+
async with session.post(url, data=form) as r:
|
|
79
|
+
data = await r.json()
|
|
80
|
+
else:
|
|
81
|
+
filename = getattr(media, "name", "file")
|
|
82
|
+
form.add_field("media", media, filename=Path(filename).name)
|
|
83
|
+
async with session.post(url, data=form) as r:
|
|
84
|
+
data = await r.json()
|
|
85
|
+
if not data.get("ok"):
|
|
86
|
+
raise BotError(data.get("error_code", 0), data.get("description", "Unknown"))
|
|
87
|
+
return data["result"]
|
|
88
|
+
|
|
89
|
+
def _markup(self, reply_markup) -> Optional[dict]:
|
|
90
|
+
if reply_markup is None:
|
|
91
|
+
return None
|
|
92
|
+
return reply_markup.to_dict() if hasattr(reply_markup, "to_dict") else reply_markup
|
|
93
|
+
|
|
94
|
+
async def get_me(self) -> User:
|
|
95
|
+
return User.from_dict(await self._get("getMe"))
|
|
96
|
+
|
|
97
|
+
async def send_message(
|
|
98
|
+
self,
|
|
99
|
+
chat_id: int,
|
|
100
|
+
text: str,
|
|
101
|
+
reply_to_message_id: Optional[int] = None,
|
|
102
|
+
reply_markup: Optional[Union[InlineKeyboardMarkup, dict]] = None,
|
|
103
|
+
) -> Message:
|
|
104
|
+
return Message.from_dict(
|
|
105
|
+
await self._post(
|
|
106
|
+
"sendMessage",
|
|
107
|
+
chat_id=chat_id,
|
|
108
|
+
text=text,
|
|
109
|
+
reply_to_message_id=reply_to_message_id,
|
|
110
|
+
reply_markup=self._markup(reply_markup),
|
|
111
|
+
),
|
|
112
|
+
self,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
async def send_photo(
|
|
116
|
+
self,
|
|
117
|
+
chat_id: int,
|
|
118
|
+
media: MediaInput,
|
|
119
|
+
caption: Optional[str] = None,
|
|
120
|
+
reply_to_message_id: Optional[int] = None,
|
|
121
|
+
) -> Message:
|
|
122
|
+
return Message.from_dict(
|
|
123
|
+
await self._post_media("sendPhoto", chat_id, media, caption, reply_to_message_id),
|
|
124
|
+
self,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
async def send_document(
|
|
128
|
+
self,
|
|
129
|
+
chat_id: int,
|
|
130
|
+
media: MediaInput,
|
|
131
|
+
caption: Optional[str] = None,
|
|
132
|
+
reply_to_message_id: Optional[int] = None,
|
|
133
|
+
) -> Message:
|
|
134
|
+
return Message.from_dict(
|
|
135
|
+
await self._post_media("sendDocument", chat_id, media, caption, reply_to_message_id),
|
|
136
|
+
self,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
async def send_video(
|
|
140
|
+
self,
|
|
141
|
+
chat_id: int,
|
|
142
|
+
media: MediaInput,
|
|
143
|
+
caption: Optional[str] = None,
|
|
144
|
+
reply_to_message_id: Optional[int] = None,
|
|
145
|
+
) -> Message:
|
|
146
|
+
return Message.from_dict(
|
|
147
|
+
await self._post_media("sendVideo", chat_id, media, caption, reply_to_message_id),
|
|
148
|
+
self,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
async def send_audio(
|
|
152
|
+
self,
|
|
153
|
+
chat_id: int,
|
|
154
|
+
media: MediaInput,
|
|
155
|
+
caption: Optional[str] = None,
|
|
156
|
+
reply_to_message_id: Optional[int] = None,
|
|
157
|
+
) -> Message:
|
|
158
|
+
return Message.from_dict(
|
|
159
|
+
await self._post_media("sendAudio", chat_id, media, caption, reply_to_message_id),
|
|
160
|
+
self,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
async def edit_message_text(
|
|
164
|
+
self,
|
|
165
|
+
chat_id: int,
|
|
166
|
+
message_id: int,
|
|
167
|
+
text: str,
|
|
168
|
+
reply_markup: Optional[Union[InlineKeyboardMarkup, dict]] = None,
|
|
169
|
+
) -> Message:
|
|
170
|
+
return Message.from_dict(
|
|
171
|
+
await self._post("editMessageText", chat_id=chat_id, message_id=message_id,
|
|
172
|
+
text=text, reply_markup=self._markup(reply_markup)),
|
|
173
|
+
self,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
async def delete_message(self, chat_id: int, message_id: int) -> bool:
|
|
177
|
+
return bool(await self._post("deleteMessage", chat_id=chat_id, message_id=message_id))
|
|
178
|
+
|
|
179
|
+
async def get_updates(
|
|
180
|
+
self,
|
|
181
|
+
offset: int = 0,
|
|
182
|
+
timeout: int = 20,
|
|
183
|
+
limit: int = 100,
|
|
184
|
+
) -> List[Update]:
|
|
185
|
+
result = await self._get("getUpdates", offset=offset, timeout=timeout, limit=limit)
|
|
186
|
+
return [Update.from_dict(u, self) for u in result]
|
|
187
|
+
|
|
188
|
+
async def answer_callback_query(self, callback_query_id: str) -> bool:
|
|
189
|
+
return bool(await self._post("answerCallbackQuery", callback_query_id=callback_query_id))
|
|
190
|
+
|
|
191
|
+
async def set_my_commands(self, commands: List[BotCommand]) -> bool:
|
|
192
|
+
return bool(await self._post("setMyCommands", commands=[c.to_dict() for c in commands]))
|
|
193
|
+
|
|
194
|
+
async def get_my_commands(self) -> List[BotCommand]:
|
|
195
|
+
result = await self._get("getMyCommands")
|
|
196
|
+
return [BotCommand(c["command"], c["description"]) for c in result]
|
|
197
|
+
|
|
198
|
+
async def set_my_name(self, name: str) -> bool:
|
|
199
|
+
return bool(await self._post("setMyName", name=name))
|
|
200
|
+
|
|
201
|
+
async def set_my_description(self, description: str) -> bool:
|
|
202
|
+
return bool(await self._post("setMyDescription", description=description))
|
|
203
|
+
|
|
204
|
+
async def set_my_short_description(self, short_description: str) -> bool:
|
|
205
|
+
return bool(await self._post("setMyShortDescription", short_description=short_description))
|
|
206
|
+
|
|
207
|
+
async def get_chat(self, chat_id: int) -> Chat:
|
|
208
|
+
return Chat.from_dict(await self._get("getChat", chat_id=chat_id))
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Callable, List, Tuple
|
|
5
|
+
|
|
6
|
+
from .bot import Bot, BotError
|
|
7
|
+
from .filters import Filter
|
|
8
|
+
from .types import CallbackQuery, Message, Update
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
HandlerList = List[Tuple[List[Filter], Callable]]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Router:
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self._message_handlers: HandlerList = []
|
|
18
|
+
self._edited_message_handlers: HandlerList = []
|
|
19
|
+
self._callback_query_handlers: HandlerList = []
|
|
20
|
+
|
|
21
|
+
def message(self, *filters: Filter):
|
|
22
|
+
def decorator(func: Callable):
|
|
23
|
+
self._message_handlers.append((list(filters), func))
|
|
24
|
+
return func
|
|
25
|
+
return decorator
|
|
26
|
+
|
|
27
|
+
def edited_message(self, *filters: Filter):
|
|
28
|
+
def decorator(func: Callable):
|
|
29
|
+
self._edited_message_handlers.append((list(filters), func))
|
|
30
|
+
return func
|
|
31
|
+
return decorator
|
|
32
|
+
|
|
33
|
+
def callback_query(self, *filters: Filter):
|
|
34
|
+
def decorator(func: Callable):
|
|
35
|
+
self._callback_query_handlers.append((list(filters), func))
|
|
36
|
+
return func
|
|
37
|
+
return decorator
|
|
38
|
+
|
|
39
|
+
async def _run_handlers(self, handlers: HandlerList, obj) -> None:
|
|
40
|
+
for filters, handler in handlers:
|
|
41
|
+
if all([await f(obj) for f in filters]):
|
|
42
|
+
try:
|
|
43
|
+
await handler(obj)
|
|
44
|
+
except Exception:
|
|
45
|
+
logger.exception("Ошибка в обработчике %s", handler.__name__)
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
async def process_update(self, update: Update) -> None:
|
|
49
|
+
if update.message:
|
|
50
|
+
await self._run_handlers(self._message_handlers, update.message)
|
|
51
|
+
elif update.edited_message:
|
|
52
|
+
await self._run_handlers(self._edited_message_handlers, update.edited_message)
|
|
53
|
+
elif update.callback_query:
|
|
54
|
+
await self._run_handlers(self._callback_query_handlers, update.callback_query)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Dispatcher(Router):
|
|
58
|
+
async def start_polling(self, bot: Bot, timeout: int = 20) -> None:
|
|
59
|
+
offset = 0
|
|
60
|
+
logger.info("Поллинг запущен")
|
|
61
|
+
try:
|
|
62
|
+
while True:
|
|
63
|
+
try:
|
|
64
|
+
updates = await bot.get_updates(offset=offset, timeout=timeout)
|
|
65
|
+
for update in updates:
|
|
66
|
+
offset = update.update_id + 1
|
|
67
|
+
await self.process_update(update)
|
|
68
|
+
except BotError as e:
|
|
69
|
+
logger.error("Ошибка API: %s", e)
|
|
70
|
+
await asyncio.sleep(1)
|
|
71
|
+
except asyncio.CancelledError:
|
|
72
|
+
raise
|
|
73
|
+
except Exception:
|
|
74
|
+
logger.exception("Ошибка поллинга")
|
|
75
|
+
await asyncio.sleep(1)
|
|
76
|
+
finally:
|
|
77
|
+
await bot.close()
|
|
78
|
+
logger.info("Поллинг остановлен")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import re
|
|
3
|
+
from typing import Union
|
|
4
|
+
|
|
5
|
+
from .types import CallbackQuery, Message
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Filter:
|
|
9
|
+
async def __call__(self, obj) -> bool:
|
|
10
|
+
raise NotImplementedError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CommandFilter(Filter):
|
|
14
|
+
def __init__(self, *commands: str):
|
|
15
|
+
self.commands = {c.lstrip("/") for c in commands}
|
|
16
|
+
|
|
17
|
+
async def __call__(self, msg: Message) -> bool:
|
|
18
|
+
if not msg.text:
|
|
19
|
+
return False
|
|
20
|
+
first = msg.text.split()[0].lstrip("/").split("@")[0]
|
|
21
|
+
return first in self.commands
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TextFilter(Filter):
|
|
25
|
+
def __init__(self, pattern: Union[str, re.Pattern]):
|
|
26
|
+
self.pattern = re.compile(pattern) if isinstance(pattern, str) else pattern
|
|
27
|
+
|
|
28
|
+
async def __call__(self, msg: Message) -> bool:
|
|
29
|
+
return bool(msg.text and self.pattern.search(msg.text))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CallbackDataFilter(Filter):
|
|
33
|
+
def __init__(self, data: str):
|
|
34
|
+
self.data = data
|
|
35
|
+
|
|
36
|
+
async def __call__(self, cb: CallbackQuery) -> bool:
|
|
37
|
+
return cb.data == self.data
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CallbackDataPrefixFilter(Filter):
|
|
41
|
+
def __init__(self, prefix: str):
|
|
42
|
+
self.prefix = prefix
|
|
43
|
+
|
|
44
|
+
async def __call__(self, cb: CallbackQuery) -> bool:
|
|
45
|
+
return cb.data is not None and cb.data.startswith(self.prefix)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def command(*commands: str) -> CommandFilter:
|
|
49
|
+
return CommandFilter(*commands)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def text(pattern: Union[str, re.Pattern]) -> TextFilter:
|
|
53
|
+
return TextFilter(pattern)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def callback_data(data: str) -> CallbackDataFilter:
|
|
57
|
+
return CallbackDataFilter(data)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def callback_data_prefix(prefix: str) -> CallbackDataPrefixFilter:
|
|
61
|
+
return CallbackDataPrefixFilter(prefix)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Any, List, Optional, TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .bot import Bot
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class User:
|
|
11
|
+
id: int
|
|
12
|
+
is_bot: bool
|
|
13
|
+
first_name: str
|
|
14
|
+
username: Optional[str] = None
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_dict(cls, d: dict) -> "User":
|
|
18
|
+
return cls(
|
|
19
|
+
id=d["id"],
|
|
20
|
+
is_bot=d.get("is_bot", False),
|
|
21
|
+
first_name=d.get("first_name", ""),
|
|
22
|
+
username=d.get("username"),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Chat:
|
|
28
|
+
id: int
|
|
29
|
+
type: str
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_dict(cls, d: dict) -> "Chat":
|
|
33
|
+
return cls(id=d["id"], type=d.get("type", ""))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class InlineKeyboardButton:
|
|
38
|
+
text: str
|
|
39
|
+
callback_data: Optional[str] = None
|
|
40
|
+
url: Optional[str] = None
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> dict:
|
|
43
|
+
d: dict = {"text": self.text}
|
|
44
|
+
if self.callback_data is not None:
|
|
45
|
+
d["callback_data"] = self.callback_data
|
|
46
|
+
if self.url is not None:
|
|
47
|
+
d["url"] = self.url
|
|
48
|
+
return d
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class InlineKeyboardMarkup:
|
|
53
|
+
inline_keyboard: List[List[InlineKeyboardButton]] = field(default_factory=list)
|
|
54
|
+
|
|
55
|
+
def to_dict(self) -> dict:
|
|
56
|
+
return {
|
|
57
|
+
"inline_keyboard": [
|
|
58
|
+
[btn.to_dict() for btn in row]
|
|
59
|
+
for row in self.inline_keyboard
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class Message:
|
|
66
|
+
message_id: int
|
|
67
|
+
chat: Chat
|
|
68
|
+
date: int
|
|
69
|
+
from_user: Optional[User] = None
|
|
70
|
+
text: Optional[str] = None
|
|
71
|
+
_bot: Any = field(default=None, repr=False, compare=False)
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_dict(cls, d: dict, bot: Any = None) -> "Message":
|
|
75
|
+
return cls(
|
|
76
|
+
message_id=d["message_id"],
|
|
77
|
+
chat=Chat.from_dict(d["chat"]),
|
|
78
|
+
date=d.get("date", 0),
|
|
79
|
+
from_user=User.from_dict(d["from"]) if "from" in d else None,
|
|
80
|
+
text=d.get("text"),
|
|
81
|
+
_bot=bot,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
async def answer(self, text: str, **kwargs) -> "Message":
|
|
85
|
+
return await self._bot.send_message(self.chat.id, text, **kwargs)
|
|
86
|
+
|
|
87
|
+
async def reply(self, text: str, **kwargs) -> "Message":
|
|
88
|
+
return await self._bot.send_message(
|
|
89
|
+
self.chat.id, text, reply_to_message_id=self.message_id, **kwargs
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class CallbackQuery:
|
|
95
|
+
id: str
|
|
96
|
+
from_user: User
|
|
97
|
+
data: Optional[str] = None
|
|
98
|
+
message: Optional[Message] = None
|
|
99
|
+
_bot: Any = field(default=None, repr=False, compare=False)
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def from_dict(cls, d: dict, bot: Any = None) -> "CallbackQuery":
|
|
103
|
+
return cls(
|
|
104
|
+
id=d["id"],
|
|
105
|
+
from_user=User.from_dict(d["from"]),
|
|
106
|
+
data=d.get("data"),
|
|
107
|
+
message=Message.from_dict(d["message"], bot) if "message" in d else None,
|
|
108
|
+
_bot=bot,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
async def answer(self) -> None:
|
|
112
|
+
await self._bot.answer_callback_query(self.id)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class Update:
|
|
117
|
+
update_id: int
|
|
118
|
+
message: Optional[Message] = None
|
|
119
|
+
edited_message: Optional[Message] = None
|
|
120
|
+
callback_query: Optional[CallbackQuery] = None
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def from_dict(cls, d: dict, bot: Any = None) -> "Update":
|
|
124
|
+
return cls(
|
|
125
|
+
update_id=d["update_id"],
|
|
126
|
+
message=Message.from_dict(d["message"], bot) if "message" in d else None,
|
|
127
|
+
edited_message=Message.from_dict(d["edited_message"], bot) if "edited_message" in d else None,
|
|
128
|
+
callback_query=CallbackQuery.from_dict(d["callback_query"], bot) if "callback_query" in d else None,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass
|
|
133
|
+
class BotCommand:
|
|
134
|
+
command: str
|
|
135
|
+
description: str
|
|
136
|
+
|
|
137
|
+
def to_dict(self) -> dict:
|
|
138
|
+
return {"command": self.command, "description": self.description}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pykodaribot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK для создания ботов в Kodari Messenger
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://msg.kodari.ru
|
|
7
|
+
Project-URL: Documentation, https://docs.kodari.ru/
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Framework :: AsyncIO
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: aiohttp>=3.9
|
|
15
|
+
|
|
16
|
+
# pykodaribot
|
|
17
|
+
|
|
18
|
+
Python SDK для создания ботов в [Kodari Messenger](https://msg.kodari.ru).
|
|
19
|
+
|
|
20
|
+
**Документация:** [docs.kodari.ru](https://docs.kodari.ru/)
|
|
21
|
+
|
|
22
|
+
## Установка
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install pykodaribot
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Требования:** Python 3.9+
|
|
29
|
+
|
|
30
|
+
## Быстрый старт
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import asyncio
|
|
34
|
+
from pykodaribot import Bot, Dispatcher, filters
|
|
35
|
+
|
|
36
|
+
bot = Bot("bot_ВАШ_ТОКЕН")
|
|
37
|
+
dp = Dispatcher()
|
|
38
|
+
|
|
39
|
+
@dp.message(filters.command("start"))
|
|
40
|
+
async def start(msg):
|
|
41
|
+
await msg.answer("Привет!")
|
|
42
|
+
|
|
43
|
+
@dp.message()
|
|
44
|
+
async def echo(msg):
|
|
45
|
+
if msg.text:
|
|
46
|
+
await msg.reply(msg.text)
|
|
47
|
+
|
|
48
|
+
asyncio.run(dp.start_polling(bot))
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Токен
|
|
52
|
+
|
|
53
|
+
Напишите @BotFather в мессенджере, введите `/newbot` и следуйте инструкциям.
|
|
54
|
+
|
|
55
|
+
## API
|
|
56
|
+
|
|
57
|
+
### Bot
|
|
58
|
+
|
|
59
|
+
| Метод | Описание |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `get_me()` | Информация о боте |
|
|
62
|
+
| `send_message(chat_id, text, reply_to_message_id, reply_markup)` | Отправить текст |
|
|
63
|
+
| `send_photo(chat_id, media, caption, reply_to_message_id)` | Отправить фото |
|
|
64
|
+
| `send_document(chat_id, media, caption, reply_to_message_id)` | Отправить файл |
|
|
65
|
+
| `send_video(chat_id, media, caption, reply_to_message_id)` | Отправить видео |
|
|
66
|
+
| `send_audio(chat_id, media, caption, reply_to_message_id)` | Отправить аудио |
|
|
67
|
+
| `edit_message_text(chat_id, message_id, text, reply_markup)` | Редактировать сообщение |
|
|
68
|
+
| `delete_message(chat_id, message_id)` | Удалить сообщение |
|
|
69
|
+
| `get_updates(offset, timeout, limit)` | Получить апдейты |
|
|
70
|
+
| `answer_callback_query(callback_query_id)` | Ответить на callback |
|
|
71
|
+
| `set_my_commands(commands)` | Установить команды меню |
|
|
72
|
+
| `get_my_commands()` | Получить команды меню |
|
|
73
|
+
| `set_my_name(name)` | Изменить имя бота |
|
|
74
|
+
| `set_my_description(description)` | Изменить описание |
|
|
75
|
+
| `set_my_short_description(short_description)` | Изменить краткое описание |
|
|
76
|
+
| `get_chat(chat_id)` | Информация о чате |
|
|
77
|
+
|
|
78
|
+
### Dispatcher
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
dp = Dispatcher()
|
|
82
|
+
|
|
83
|
+
@dp.message(*filters) # новое сообщение
|
|
84
|
+
@dp.edited_message(*filters) # изменённое сообщение
|
|
85
|
+
@dp.callback_query(*filters) # нажатие inline-кнопки
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Фильтры
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from pykodaribot import filters
|
|
92
|
+
|
|
93
|
+
filters.command("start", "help") # команды /start или /help
|
|
94
|
+
filters.text(r"привет|hello") # regex по тексту
|
|
95
|
+
filters.callback_data("yes") # точное совпадение callback_data
|
|
96
|
+
filters.callback_data_prefix("action:") # callback_data начинается с префикса
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Inline-клавиатура
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from pykodaribot import InlineKeyboardMarkup, InlineKeyboardButton
|
|
103
|
+
|
|
104
|
+
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
|
105
|
+
[
|
|
106
|
+
InlineKeyboardButton(text="Да", callback_data="yes"),
|
|
107
|
+
InlineKeyboardButton(text="Нет", callback_data="no"),
|
|
108
|
+
],
|
|
109
|
+
[
|
|
110
|
+
InlineKeyboardButton(text="Открыть сайт", url="https://kodari.ru"),
|
|
111
|
+
],
|
|
112
|
+
])
|
|
113
|
+
|
|
114
|
+
await msg.answer("Выберите:", reply_markup=keyboard)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Отправка медиа
|
|
118
|
+
|
|
119
|
+
`media` принимает путь к файлу, `Path`, `bytes` или file-like объект:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
await bot.send_photo(chat_id, "photo.jpg", caption="Смотри!")
|
|
123
|
+
await bot.send_photo(chat_id, Path("/tmp/image.png"))
|
|
124
|
+
await bot.send_document(chat_id, open("report.pdf", "rb"))
|
|
125
|
+
|
|
126
|
+
with open("clip.mp4", "rb") as f:
|
|
127
|
+
await bot.send_video(chat_id, f, caption="Видео")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Примеры
|
|
131
|
+
|
|
132
|
+
- [`examples/echo_bot.py`](examples/echo_bot.py) — эхо-бот
|
|
133
|
+
- [`examples/inline_bot.py`](examples/inline_bot.py) — inline-кнопки и счётчик
|
|
134
|
+
- [`examples/commands_bot.py`](examples/commands_bot.py) — команды меню
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
pykodaribot/__init__.py
|
|
4
|
+
pykodaribot/bot.py
|
|
5
|
+
pykodaribot/dispatcher.py
|
|
6
|
+
pykodaribot/filters.py
|
|
7
|
+
pykodaribot/types.py
|
|
8
|
+
pykodaribot.egg-info/PKG-INFO
|
|
9
|
+
pykodaribot.egg-info/SOURCES.txt
|
|
10
|
+
pykodaribot.egg-info/dependency_links.txt
|
|
11
|
+
pykodaribot.egg-info/requires.txt
|
|
12
|
+
pykodaribot.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aiohttp>=3.9
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pykodaribot
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pykodaribot"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK для создания ботов в Kodari Messenger"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
dependencies = ["aiohttp>=3.9"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Framework :: AsyncIO",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://msg.kodari.ru"
|
|
22
|
+
Documentation = "https://docs.kodari.ru/"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.packages.find]
|
|
25
|
+
where = ["."]
|
|
26
|
+
include = ["pykodaribot*"]
|