neko-speaker 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.
- neko_speaker-0.1.0/LICENSE +21 -0
- neko_speaker-0.1.0/PKG-INFO +105 -0
- neko_speaker-0.1.0/README.md +82 -0
- neko_speaker-0.1.0/pyproject.toml +37 -0
- neko_speaker-0.1.0/setup.cfg +4 -0
- neko_speaker-0.1.0/src/neko_speaker/__init__.py +0 -0
- neko_speaker-0.1.0/src/neko_speaker/auth.py +188 -0
- neko_speaker-0.1.0/src/neko_speaker/bot_types.py +270 -0
- neko_speaker-0.1.0/src/neko_speaker/exceptions.py +65 -0
- neko_speaker-0.1.0/src/neko_speaker/test.py +19 -0
- neko_speaker-0.1.0/src/neko_speaker/utils.py +1681 -0
- neko_speaker-0.1.0/src/neko_speaker.egg-info/PKG-INFO +105 -0
- neko_speaker-0.1.0/src/neko_speaker.egg-info/SOURCES.txt +14 -0
- neko_speaker-0.1.0/src/neko_speaker.egg-info/dependency_links.txt +1 -0
- neko_speaker-0.1.0/src/neko_speaker.egg-info/requires.txt +11 -0
- neko_speaker-0.1.0/src/neko_speaker.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fukuneko
|
|
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.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: neko-speaker
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Асинхронный клиент VK API с удобной обёрткой для ботов сообществ для быстрого развертывания ботов
|
|
5
|
+
Author-email: Fukuneko <lcotyt112@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/LuckyCot112/maneki2
|
|
8
|
+
Keywords: vk,vk-api,bot,async,neko
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: aiohttp>=3.8
|
|
13
|
+
Requires-Dist: typing-extensions
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
17
|
+
Requires-Dist: aresponses; extra == "dev"
|
|
18
|
+
Requires-Dist: black; extra == "dev"
|
|
19
|
+
Requires-Dist: flake8; extra == "dev"
|
|
20
|
+
Requires-Dist: mypy; extra == "dev"
|
|
21
|
+
Requires-Dist: python-dotenv; extra == "dev"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# neko-speaker
|
|
26
|
+
|
|
27
|
+
Асинхронный клиент для работы с API социальных платформ. Библиотека предоставляет удобный интерфейс для взаимодействия с API VK и упрощает разработку ботов и интеграций. В будущем планируется поддержка дополнительных платформ.
|
|
28
|
+
|
|
29
|
+
## Установка
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install neko-speaker
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Для разработки с тестовыми и вспомогательными инструментами:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install "neko-speaker[dev]"
|
|
39
|
+
```
|
|
40
|
+
Быстрый старт
|
|
41
|
+
Вставьте сюда ваш пример кода для быстрого начала работы:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# Пример использования
|
|
45
|
+
import os
|
|
46
|
+
import asyncio
|
|
47
|
+
import aiohttp
|
|
48
|
+
import logging
|
|
49
|
+
|
|
50
|
+
from src.neko_speaker.auth import VkBotLongPoll
|
|
51
|
+
|
|
52
|
+
# Create your own event handling file
|
|
53
|
+
from example_handler import EventHandler
|
|
54
|
+
|
|
55
|
+
from src.neko_speaker.exceptions import *
|
|
56
|
+
from dotenv import load_dotenv
|
|
57
|
+
load_dotenv()
|
|
58
|
+
|
|
59
|
+
# use only if you need debugging
|
|
60
|
+
logging.basicConfig(
|
|
61
|
+
level=logging.DEBUG,
|
|
62
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# create .env file with this constantas
|
|
66
|
+
TOKEN = os.getenv("VkLongpollBotAPIToken")
|
|
67
|
+
GROUP_ID = os.getenv("GroupID")
|
|
68
|
+
|
|
69
|
+
async def main():
|
|
70
|
+
async with VkBotLongPoll(token=TOKEN, group_id=GROUP_ID) as bot:
|
|
71
|
+
await bot.update_longpoll_server()
|
|
72
|
+
print("LongPoll инициализирован")
|
|
73
|
+
|
|
74
|
+
while bot._is_work:
|
|
75
|
+
events = await bot.listen()
|
|
76
|
+
for event in events:
|
|
77
|
+
try:
|
|
78
|
+
await EventHandler.event_handler(bot, event)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
print(f"Ошибка при обработке события: {e}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
asyncio.run(main())
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
Основные возможности
|
|
89
|
+
Асинхронная работа на базе aiohttp.
|
|
90
|
+
Структурированный доступ к методам API по разделам (сообщения, стена, фото и др.).
|
|
91
|
+
Обработка ошибок и типовых сценариев ответов API.
|
|
92
|
+
Типизированные интерфейсы для предсказуемой работы с данными.
|
|
93
|
+
Тестирование
|
|
94
|
+
Для запуска тестов используйте:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pytest
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
Инструменты для проверки стиля и типов (black, flake8, mypy) включены в группу зависимостей dev.
|
|
102
|
+
|
|
103
|
+
Вклад в проект
|
|
104
|
+
Предложения по улучшению, отчёты об ошибках и запросы на добавление функционала принимаются через систему Issues в репозитории. Для удобства используйте предоставленные шаблоны.
|
|
105
|
+
https://github.com/LuckyCot112/maneki2/issues
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
|
|
2
|
+
# neko-speaker
|
|
3
|
+
|
|
4
|
+
Асинхронный клиент для работы с API социальных платформ. Библиотека предоставляет удобный интерфейс для взаимодействия с API VK и упрощает разработку ботов и интеграций. В будущем планируется поддержка дополнительных платформ.
|
|
5
|
+
|
|
6
|
+
## Установка
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install neko-speaker
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Для разработки с тестовыми и вспомогательными инструментами:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install "neko-speaker[dev]"
|
|
16
|
+
```
|
|
17
|
+
Быстрый старт
|
|
18
|
+
Вставьте сюда ваш пример кода для быстрого начала работы:
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
# Пример использования
|
|
22
|
+
import os
|
|
23
|
+
import asyncio
|
|
24
|
+
import aiohttp
|
|
25
|
+
import logging
|
|
26
|
+
|
|
27
|
+
from src.neko_speaker.auth import VkBotLongPoll
|
|
28
|
+
|
|
29
|
+
# Create your own event handling file
|
|
30
|
+
from example_handler import EventHandler
|
|
31
|
+
|
|
32
|
+
from src.neko_speaker.exceptions import *
|
|
33
|
+
from dotenv import load_dotenv
|
|
34
|
+
load_dotenv()
|
|
35
|
+
|
|
36
|
+
# use only if you need debugging
|
|
37
|
+
logging.basicConfig(
|
|
38
|
+
level=logging.DEBUG,
|
|
39
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# create .env file with this constantas
|
|
43
|
+
TOKEN = os.getenv("VkLongpollBotAPIToken")
|
|
44
|
+
GROUP_ID = os.getenv("GroupID")
|
|
45
|
+
|
|
46
|
+
async def main():
|
|
47
|
+
async with VkBotLongPoll(token=TOKEN, group_id=GROUP_ID) as bot:
|
|
48
|
+
await bot.update_longpoll_server()
|
|
49
|
+
print("LongPoll инициализирован")
|
|
50
|
+
|
|
51
|
+
while bot._is_work:
|
|
52
|
+
events = await bot.listen()
|
|
53
|
+
for event in events:
|
|
54
|
+
try:
|
|
55
|
+
await EventHandler.event_handler(bot, event)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print(f"Ошибка при обработке события: {e}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
asyncio.run(main())
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
Основные возможности
|
|
66
|
+
Асинхронная работа на базе aiohttp.
|
|
67
|
+
Структурированный доступ к методам API по разделам (сообщения, стена, фото и др.).
|
|
68
|
+
Обработка ошибок и типовых сценариев ответов API.
|
|
69
|
+
Типизированные интерфейсы для предсказуемой работы с данными.
|
|
70
|
+
Тестирование
|
|
71
|
+
Для запуска тестов используйте:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pytest
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
Инструменты для проверки стиля и типов (black, flake8, mypy) включены в группу зависимостей dev.
|
|
79
|
+
|
|
80
|
+
Вклад в проект
|
|
81
|
+
Предложения по улучшению, отчёты об ошибках и запросы на добавление функционала принимаются через систему Issues в репозитории. Для удобства используйте предоставленные шаблоны.
|
|
82
|
+
https://github.com/LuckyCot112/maneki2/issues
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "neko-speaker"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Асинхронный клиент VK API с удобной обёрткой для ботов сообществ для быстрого развертывания ботов"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Fukuneko", email = "lcotyt112@gmail.com" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["vk", "vk-api", "bot", "async", "neko"]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"aiohttp>=3.8",
|
|
18
|
+
"typing-extensions"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
dev = [
|
|
23
|
+
"pytest",
|
|
24
|
+
"pytest-asyncio",
|
|
25
|
+
"aresponses",
|
|
26
|
+
"black",
|
|
27
|
+
"flake8",
|
|
28
|
+
"mypy",
|
|
29
|
+
"python-dotenv" # ← только для разработки
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Repository = "https://github.com/LuckyCot112/maneki2"
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
where = ["src"]
|
|
37
|
+
include = ["neko_speaker*"]
|
|
File without changes
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import asyncio
|
|
3
|
+
import aiohttp
|
|
4
|
+
import src.neko_speaker.bot_types as Type
|
|
5
|
+
from typing import Optional, Any, Dict, Union
|
|
6
|
+
# from dotenv import load_dotenv
|
|
7
|
+
# load_dotenv()
|
|
8
|
+
|
|
9
|
+
from src.neko_speaker.utils import Message, Attachment, Group, User
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("nekobot.longpoll")
|
|
13
|
+
|
|
14
|
+
# API = os.getenv("VkApiAddress")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class VkBotLongPoll(Type.VkBotProtocol):
|
|
18
|
+
def __init__(self, token, group_id, session: aiohttp.ClientSession | None = None, wait_time=25, preserve_original_params = True):
|
|
19
|
+
self._token = token
|
|
20
|
+
self._group_id = group_id
|
|
21
|
+
self._wait_time = wait_time
|
|
22
|
+
|
|
23
|
+
self._url = os.getenv("VkApiAddress")
|
|
24
|
+
self._key: Optional[str] = None
|
|
25
|
+
self._server: Optional[str] = None
|
|
26
|
+
self._ts: Optional[int] = None
|
|
27
|
+
self._is_work = True
|
|
28
|
+
|
|
29
|
+
# для метода message.edit. Если True, при редактировании будет сначала получать сообщение, сохраняя неуказанные при редактировании параметры.
|
|
30
|
+
# Если передать в редактирование только текст, клавиатура, вложения и прочее сохранятся при True и удалятся при False. Значение можно передавать в метод, данный параметр отвечает за стандартное значение
|
|
31
|
+
self._preserve_original_params = preserve_original_params
|
|
32
|
+
self._session = session
|
|
33
|
+
|
|
34
|
+
self.message = Message(self)
|
|
35
|
+
self.attachment = Attachment(self)
|
|
36
|
+
self.group = Group(self)
|
|
37
|
+
self.user = User(self)
|
|
38
|
+
|
|
39
|
+
if session is None:
|
|
40
|
+
self._session = None
|
|
41
|
+
|
|
42
|
+
# asyncio.create_task(self.update_longpoll_server())
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def __aenter__(self):
|
|
46
|
+
if self._session is None:
|
|
47
|
+
self._session = aiohttp.ClientSession()
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def __aexit__(self, exc_type, exc, tb):
|
|
52
|
+
if self._session:
|
|
53
|
+
await self._session.close()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def session(self) -> aiohttp.ClientSession:
|
|
58
|
+
if self._session is None:
|
|
59
|
+
raise RuntimeError(
|
|
60
|
+
f"Сессия не инициализирована в {self.__class__.__name__}. "
|
|
61
|
+
"Используйте async with VKBot(...) as bot:"
|
|
62
|
+
)
|
|
63
|
+
return self._session
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# __slots__ = ('vk', 'wait', 'group_id', 'url', 'session', 'key', 'server', 'ts')
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def update_longpoll_server(self, update_ts=True) -> Dict[str, Any]:
|
|
70
|
+
"""
|
|
71
|
+
Обновляет данные Long Poll сервера.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
update_ts: Если True, обновляет self._ts из ответа VK.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
dict: Полный объект response от VK API.
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
ValueError: Если VK вернул ошибку.
|
|
81
|
+
aiohttp.ClientError: При сетевых ошибках.
|
|
82
|
+
RuntimeError: При проблемах с JSON.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
logger.debug("Starting update_longpoll_server (update_ts=%s)", update_ts)
|
|
86
|
+
url = f"{self._url}groups.getLongPollServer"
|
|
87
|
+
params = {
|
|
88
|
+
"access_token": self._token,
|
|
89
|
+
"group_id": self._group_id,
|
|
90
|
+
"v": "5.199",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async with self.session.get(url, params=params) as response:
|
|
94
|
+
response.raise_for_status()
|
|
95
|
+
data = await response.json()
|
|
96
|
+
|
|
97
|
+
if 'error' in data:
|
|
98
|
+
err = data["error"]
|
|
99
|
+
logger.error("VK API error in groups.getLongPollServer: %s (%s)",
|
|
100
|
+
err.get('error_code'), err.get('error_msg'))
|
|
101
|
+
raise ValueError(f"VK API error: {err.get('error_code')} - {err.get('error_msg')}")
|
|
102
|
+
|
|
103
|
+
response = data['response']
|
|
104
|
+
self._key = response['key']
|
|
105
|
+
self._server = response['server']
|
|
106
|
+
|
|
107
|
+
if update_ts:
|
|
108
|
+
self._ts = response['ts']
|
|
109
|
+
logger.info("Long Poll server updated: key=%s, server=%s, ts=%s",
|
|
110
|
+
self._key, self._server, self._ts)
|
|
111
|
+
else:
|
|
112
|
+
logger.info("Long Poll credentials updated (ts preserved): key=%s, server=%s",
|
|
113
|
+
self._key, self._server)
|
|
114
|
+
|
|
115
|
+
return response
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def listen(self) -> Union[Type.VkEvent, Type.VkError]:
|
|
119
|
+
"""
|
|
120
|
+
Запускает прослушивание событий сервера.
|
|
121
|
+
Автоматически обновляет сервер при истечении времени ожидания и ошибках получения данных.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
dict: Полный объект события от VK API.
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
ValueError: Если VK вернул ошибку.
|
|
128
|
+
aiohttp.ClientError: При сетевых ошибках.
|
|
129
|
+
RuntimeError: При проблемах с JSON.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
logger.debug("Starting Long Poll request with ts=%s", self._ts)
|
|
133
|
+
params={
|
|
134
|
+
'act': 'a_check',
|
|
135
|
+
'key': self._key,
|
|
136
|
+
'ts': self._ts,
|
|
137
|
+
'wait': self._wait_time
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
async with self.session.get(self._server, params=params, timeout=self._wait_time + 10) as response:
|
|
142
|
+
response.raise_for_status()
|
|
143
|
+
data: Union[Type.VkEvent, Type.VkError] = await response.json()
|
|
144
|
+
except aiohttp.ClientError as e:
|
|
145
|
+
logger.warning("Network error during Long Poll request: %s", e)
|
|
146
|
+
raise
|
|
147
|
+
except ValueError as e:
|
|
148
|
+
logger.error("JSON parsing error in Long Poll response: %s", e)
|
|
149
|
+
raise RuntimeError("Invalid JSON response from VK Long Poll server") from e
|
|
150
|
+
|
|
151
|
+
if 'error' in data:
|
|
152
|
+
err: Type.VkError = data['error']
|
|
153
|
+
logger.error("VK API error in Long Poll: %s (%s)", err.get('error_code'), err.get('error_msg'))
|
|
154
|
+
raise ValueError(f"VK API error: {err.get('error_code')} - {err.get('error_msg')}")
|
|
155
|
+
|
|
156
|
+
# self._ts = data["ts"]
|
|
157
|
+
|
|
158
|
+
failed = data.get('failed')
|
|
159
|
+
|
|
160
|
+
if failed is None:
|
|
161
|
+
self._ts = data["ts"]
|
|
162
|
+
logger.debug("Long Poll succeeded, new ts=%s, updates count=%s", self._ts, len(data.get('updates', [])))
|
|
163
|
+
return data.get('updates', [])
|
|
164
|
+
|
|
165
|
+
logger.info("Long Poll failed code=%s, handling recovery", failed)
|
|
166
|
+
|
|
167
|
+
if failed == 1:
|
|
168
|
+
self._ts = data.get('ts', self._ts)
|
|
169
|
+
logger.info("Key expired (failed=1), ts updated to %s", self._ts)
|
|
170
|
+
elif failed == 2:
|
|
171
|
+
await self.update_longpoll_server(update_ts=False)
|
|
172
|
+
logger.info("TS reset required (failed=2), server credentials updated")
|
|
173
|
+
elif failed == 3:
|
|
174
|
+
await self.update_longpoll_server()
|
|
175
|
+
logger.info("Server changed (failed=3), full Long Poll config updated")
|
|
176
|
+
else:
|
|
177
|
+
logger.warning("Unknown failed code=%s, skipping recovery logic", failed)
|
|
178
|
+
|
|
179
|
+
logger.debug("Raw failed response: %s", data)
|
|
180
|
+
return []
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
async def main():
|
|
184
|
+
print("auth.py not standalone")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
asyncio.run(main())
|