yuchatlib 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.
- yuchatlib-0.1.0/.gitignore +12 -0
- yuchatlib-0.1.0/LICENSE +21 -0
- yuchatlib-0.1.0/PKG-INFO +87 -0
- yuchatlib-0.1.0/README.md +67 -0
- yuchatlib-0.1.0/pyproject.toml +51 -0
- yuchatlib-0.1.0/src/yuchatlib/__init__.py +109 -0
- yuchatlib-0.1.0/src/yuchatlib/api/__init__.py +1 -0
- yuchatlib-0.1.0/src/yuchatlib/api/client.py +172 -0
- yuchatlib-0.1.0/src/yuchatlib/api/errors.py +25 -0
- yuchatlib-0.1.0/src/yuchatlib/api/models.py +341 -0
- yuchatlib-0.1.0/src/yuchatlib/bot.py +122 -0
- yuchatlib-0.1.0/src/yuchatlib/dispatch/__init__.py +29 -0
- yuchatlib-0.1.0/src/yuchatlib/dispatch/command.py +42 -0
- yuchatlib-0.1.0/src/yuchatlib/dispatch/context.py +75 -0
- yuchatlib-0.1.0/src/yuchatlib/dispatch/dispatcher.py +42 -0
- yuchatlib-0.1.0/src/yuchatlib/dispatch/filters.py +79 -0
- yuchatlib-0.1.0/src/yuchatlib/dispatch/router.py +85 -0
- yuchatlib-0.1.0/src/yuchatlib/facade.py +192 -0
- yuchatlib-0.1.0/src/yuchatlib/fsm/__init__.py +18 -0
- yuchatlib-0.1.0/src/yuchatlib/fsm/context.py +31 -0
- yuchatlib-0.1.0/src/yuchatlib/fsm/memory.py +95 -0
- yuchatlib-0.1.0/src/yuchatlib/fsm/state.py +26 -0
- yuchatlib-0.1.0/src/yuchatlib/fsm/storage.py +47 -0
- yuchatlib-0.1.0/src/yuchatlib/polling/__init__.py +12 -0
- yuchatlib-0.1.0/src/yuchatlib/polling/config.py +31 -0
- yuchatlib-0.1.0/src/yuchatlib/polling/offset.py +36 -0
- yuchatlib-0.1.0/src/yuchatlib/polling/worker.py +217 -0
- yuchatlib-0.1.0/src/yuchatlib/py.typed +1 -0
- yuchatlib-0.1.0/tests/smoke/test_command_fsm_flow.py +79 -0
- yuchatlib-0.1.0/tests/smoke/test_polling_flow.py +78 -0
- yuchatlib-0.1.0/tests/smoke/test_public_api.py +47 -0
yuchatlib-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dmitriy Tikhonov
|
|
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.
|
yuchatlib-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: yuchatlib
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Библиотека для Public API YuChat
|
|
5
|
+
Project-URL: Documentation, https://docs.yuchat.ru/api-docs/category/api-documentation
|
|
6
|
+
Project-URL: Source, https://github.com/codegenIrlX/yuchatlib
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Requires-Dist: aiohttp==3.14.3
|
|
11
|
+
Requires-Dist: pydantic==2.13.4
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: mypy==1.20.2; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest==9.1.1; extra == 'dev'
|
|
15
|
+
Requires-Dist: ruff==0.16.3; extra == 'dev'
|
|
16
|
+
Provides-Extra: release
|
|
17
|
+
Requires-Dist: build==1.5.0; extra == 'release'
|
|
18
|
+
Requires-Dist: twine==7.0.0; extra == 'release'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# yuchatlib
|
|
22
|
+
|
|
23
|
+
Асинхронная Python-библиотека для создания ботов YuChat.
|
|
24
|
+
|
|
25
|
+
Поддерживает long polling, команды, декораторы обработчиков, FSM, отправку и
|
|
26
|
+
редактирование сообщений. Данные API валидируются через Pydantic.
|
|
27
|
+
|
|
28
|
+
## Установка
|
|
29
|
+
|
|
30
|
+
```powershell
|
|
31
|
+
pip install yuchatlib==0.1.0
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Требуется Python 3.12 или новее.
|
|
35
|
+
|
|
36
|
+
## Быстрый старт
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import asyncio
|
|
40
|
+
|
|
41
|
+
from yuchatlib import HandlerContext, UpdateListItemResponseDto, YuChatBot
|
|
42
|
+
|
|
43
|
+
bot = YuChatBot(
|
|
44
|
+
token="your-token",
|
|
45
|
+
workspace_id="your-workspace-id",
|
|
46
|
+
base_url="https://your-yuchat-api.example",
|
|
47
|
+
polling_interval=2,
|
|
48
|
+
storage=None,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@bot.command(name="start", description="Начать работу")
|
|
53
|
+
async def start_command(
|
|
54
|
+
update: UpdateListItemResponseDto,
|
|
55
|
+
context: HandlerContext,
|
|
56
|
+
) -> None:
|
|
57
|
+
del update
|
|
58
|
+
await context.answer("👋 Привет!")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
asyncio.run(bot.run_polling())
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Отправка в указанный чат
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
await bot.send_message(
|
|
69
|
+
chat_id="target-chat-id",
|
|
70
|
+
text="Сообщение в выбранный чат",
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Возможности
|
|
75
|
+
|
|
76
|
+
- long polling через `getUpdates`;
|
|
77
|
+
- команды вида `/start`;
|
|
78
|
+
- FSM и state-handler;
|
|
79
|
+
- отправка и редактирование сообщений;
|
|
80
|
+
- явная отправка по `chat_id`;
|
|
81
|
+
- асинхронная обработка разных чатов;
|
|
82
|
+
- Pydantic-валидация API.
|
|
83
|
+
|
|
84
|
+
При `storage=None` FSM и polling offset хранятся в памяти процесса и
|
|
85
|
+
сбрасываются после перезапуска.
|
|
86
|
+
|
|
87
|
+
[Документация Public API YuChat](https://docs.yuchat.ru/api-docs/category/api-documentation)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# yuchatlib
|
|
2
|
+
|
|
3
|
+
Асинхронная Python-библиотека для создания ботов YuChat.
|
|
4
|
+
|
|
5
|
+
Поддерживает long polling, команды, декораторы обработчиков, FSM, отправку и
|
|
6
|
+
редактирование сообщений. Данные API валидируются через Pydantic.
|
|
7
|
+
|
|
8
|
+
## Установка
|
|
9
|
+
|
|
10
|
+
```powershell
|
|
11
|
+
pip install yuchatlib==0.1.0
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Требуется Python 3.12 или новее.
|
|
15
|
+
|
|
16
|
+
## Быстрый старт
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import asyncio
|
|
20
|
+
|
|
21
|
+
from yuchatlib import HandlerContext, UpdateListItemResponseDto, YuChatBot
|
|
22
|
+
|
|
23
|
+
bot = YuChatBot(
|
|
24
|
+
token="your-token",
|
|
25
|
+
workspace_id="your-workspace-id",
|
|
26
|
+
base_url="https://your-yuchat-api.example",
|
|
27
|
+
polling_interval=2,
|
|
28
|
+
storage=None,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@bot.command(name="start", description="Начать работу")
|
|
33
|
+
async def start_command(
|
|
34
|
+
update: UpdateListItemResponseDto,
|
|
35
|
+
context: HandlerContext,
|
|
36
|
+
) -> None:
|
|
37
|
+
del update
|
|
38
|
+
await context.answer("👋 Привет!")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
asyncio.run(bot.run_polling())
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Отправка в указанный чат
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
await bot.send_message(
|
|
49
|
+
chat_id="target-chat-id",
|
|
50
|
+
text="Сообщение в выбранный чат",
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Возможности
|
|
55
|
+
|
|
56
|
+
- long polling через `getUpdates`;
|
|
57
|
+
- команды вида `/start`;
|
|
58
|
+
- FSM и state-handler;
|
|
59
|
+
- отправка и редактирование сообщений;
|
|
60
|
+
- явная отправка по `chat_id`;
|
|
61
|
+
- асинхронная обработка разных чатов;
|
|
62
|
+
- Pydantic-валидация API.
|
|
63
|
+
|
|
64
|
+
При `storage=None` FSM и polling offset хранятся в памяти процесса и
|
|
65
|
+
сбрасываются после перезапуска.
|
|
66
|
+
|
|
67
|
+
[Документация Public API YuChat](https://docs.yuchat.ru/api-docs/category/api-documentation)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27,<2"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "yuchatlib"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Библиотека для Public API YuChat"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"aiohttp==3.14.3",
|
|
15
|
+
"pydantic==2.13.4",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
dev = [
|
|
20
|
+
"mypy==1.20.2",
|
|
21
|
+
"pytest==9.1.1",
|
|
22
|
+
"ruff==0.16.3",
|
|
23
|
+
]
|
|
24
|
+
release = [
|
|
25
|
+
"build==1.5.0",
|
|
26
|
+
"twine==7.0.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Documentation = "https://docs.yuchat.ru/api-docs/category/api-documentation"
|
|
31
|
+
Source = "https://github.com/codegenIrlX/yuchatlib"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/yuchatlib"]
|
|
35
|
+
|
|
36
|
+
[tool.ruff]
|
|
37
|
+
target-version = "py312"
|
|
38
|
+
line-length = 100
|
|
39
|
+
src = ["src"]
|
|
40
|
+
|
|
41
|
+
[tool.ruff.lint]
|
|
42
|
+
select = ["B", "E", "F", "I", "RUF", "SIM", "UP"]
|
|
43
|
+
ignore = ["RUF001", "RUF002", "RUF003"]
|
|
44
|
+
|
|
45
|
+
[tool.mypy]
|
|
46
|
+
python_version = "3.12"
|
|
47
|
+
strict = true
|
|
48
|
+
files = ["src"]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from yuchatlib.api.errors import (
|
|
2
|
+
YuChatAuthorizationError,
|
|
3
|
+
YuChatClientError,
|
|
4
|
+
YuChatRateLimitError,
|
|
5
|
+
YuChatTransientError,
|
|
6
|
+
YuChatUnexpectedResponseError,
|
|
7
|
+
)
|
|
8
|
+
from yuchatlib.api.models import (
|
|
9
|
+
ButtonBarRequestDto,
|
|
10
|
+
ButtonGroupRequestDto,
|
|
11
|
+
ButtonRequestDto,
|
|
12
|
+
CommandButtonRequestDto,
|
|
13
|
+
GetMeResponseDto,
|
|
14
|
+
GetUpdatesResponseDto,
|
|
15
|
+
LinkButtonRequestDto,
|
|
16
|
+
MessageActionResponseDto,
|
|
17
|
+
MessageType,
|
|
18
|
+
MessageUpdateResponseDto,
|
|
19
|
+
PressedButtonCommandResponseDto,
|
|
20
|
+
SendMessageResponseDto,
|
|
21
|
+
UpdateListItemResponseDto,
|
|
22
|
+
UpdateSetting,
|
|
23
|
+
)
|
|
24
|
+
from yuchatlib.bot import Bot
|
|
25
|
+
from yuchatlib.dispatch import (
|
|
26
|
+
Command,
|
|
27
|
+
CommandFilter,
|
|
28
|
+
Dispatcher,
|
|
29
|
+
HandlerContext,
|
|
30
|
+
HandlerContextError,
|
|
31
|
+
HandlerDecorator,
|
|
32
|
+
MessageActionFilter,
|
|
33
|
+
MessageFilter,
|
|
34
|
+
Router,
|
|
35
|
+
StateFilter,
|
|
36
|
+
UpdateFilter,
|
|
37
|
+
UpdateHandler,
|
|
38
|
+
parse_command,
|
|
39
|
+
)
|
|
40
|
+
from yuchatlib.facade import YuChatBot
|
|
41
|
+
from yuchatlib.fsm import (
|
|
42
|
+
DEFAULT_MAX_FSM_RECORDS,
|
|
43
|
+
FSMContext,
|
|
44
|
+
FSMData,
|
|
45
|
+
FSMKey,
|
|
46
|
+
FSMStorage,
|
|
47
|
+
FSMStorageError,
|
|
48
|
+
MemoryFSMStorage,
|
|
49
|
+
State,
|
|
50
|
+
StatesGroup,
|
|
51
|
+
)
|
|
52
|
+
from yuchatlib.polling import (
|
|
53
|
+
MemoryOffsetStorage,
|
|
54
|
+
OffsetStorage,
|
|
55
|
+
PollingConfig,
|
|
56
|
+
PollingWorker,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
__version__ = "0.1.0"
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
"DEFAULT_MAX_FSM_RECORDS",
|
|
63
|
+
"Bot",
|
|
64
|
+
"ButtonBarRequestDto",
|
|
65
|
+
"ButtonGroupRequestDto",
|
|
66
|
+
"ButtonRequestDto",
|
|
67
|
+
"Command",
|
|
68
|
+
"CommandButtonRequestDto",
|
|
69
|
+
"CommandFilter",
|
|
70
|
+
"Dispatcher",
|
|
71
|
+
"FSMContext",
|
|
72
|
+
"FSMData",
|
|
73
|
+
"FSMKey",
|
|
74
|
+
"FSMStorage",
|
|
75
|
+
"FSMStorageError",
|
|
76
|
+
"GetMeResponseDto",
|
|
77
|
+
"GetUpdatesResponseDto",
|
|
78
|
+
"HandlerContext",
|
|
79
|
+
"HandlerContextError",
|
|
80
|
+
"HandlerDecorator",
|
|
81
|
+
"LinkButtonRequestDto",
|
|
82
|
+
"MemoryFSMStorage",
|
|
83
|
+
"MemoryOffsetStorage",
|
|
84
|
+
"MessageActionFilter",
|
|
85
|
+
"MessageActionResponseDto",
|
|
86
|
+
"MessageFilter",
|
|
87
|
+
"MessageType",
|
|
88
|
+
"MessageUpdateResponseDto",
|
|
89
|
+
"OffsetStorage",
|
|
90
|
+
"PollingConfig",
|
|
91
|
+
"PollingWorker",
|
|
92
|
+
"PressedButtonCommandResponseDto",
|
|
93
|
+
"Router",
|
|
94
|
+
"SendMessageResponseDto",
|
|
95
|
+
"State",
|
|
96
|
+
"StateFilter",
|
|
97
|
+
"StatesGroup",
|
|
98
|
+
"UpdateFilter",
|
|
99
|
+
"UpdateHandler",
|
|
100
|
+
"UpdateListItemResponseDto",
|
|
101
|
+
"UpdateSetting",
|
|
102
|
+
"YuChatAuthorizationError",
|
|
103
|
+
"YuChatBot",
|
|
104
|
+
"YuChatClientError",
|
|
105
|
+
"YuChatRateLimitError",
|
|
106
|
+
"YuChatTransientError",
|
|
107
|
+
"YuChatUnexpectedResponseError",
|
|
108
|
+
"parse_command",
|
|
109
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Модели и ошибки Public API YuChat."""
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Асинхронный клиент Public API YuChat."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
from typing import TypeVar
|
|
7
|
+
|
|
8
|
+
import aiohttp
|
|
9
|
+
from pydantic import BaseModel, ValidationError
|
|
10
|
+
|
|
11
|
+
from yuchatlib.api.errors import (
|
|
12
|
+
YuChatAuthorizationError,
|
|
13
|
+
YuChatClientError,
|
|
14
|
+
YuChatRateLimitError,
|
|
15
|
+
YuChatTransientError,
|
|
16
|
+
YuChatUnexpectedResponseError,
|
|
17
|
+
)
|
|
18
|
+
from yuchatlib.api.models import (
|
|
19
|
+
EditMessageRequestDto,
|
|
20
|
+
GetMeResponseDto,
|
|
21
|
+
GetUpdatesRequestDto,
|
|
22
|
+
GetUpdatesResponseDto,
|
|
23
|
+
SendMessageRequestDto,
|
|
24
|
+
SendMessageResponseDto,
|
|
25
|
+
SetUpdateSettingsRequestDto,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
ModelType = TypeVar("ModelType", bound=BaseModel)
|
|
29
|
+
AUTHORIZATION_ERROR_STATUSES = frozenset({401, 403})
|
|
30
|
+
RATE_LIMIT_STATUS = 429
|
|
31
|
+
SERVER_ERROR_STATUS = 500
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class YuChatApiClient:
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
base_url: str,
|
|
39
|
+
token: str,
|
|
40
|
+
timeout_seconds: float,
|
|
41
|
+
connection_limit: int,
|
|
42
|
+
) -> None:
|
|
43
|
+
self._base_url = base_url.rstrip("/")
|
|
44
|
+
self._token = token
|
|
45
|
+
self._timeout_seconds = timeout_seconds
|
|
46
|
+
self._connection_limit = connection_limit
|
|
47
|
+
self._session: aiohttp.ClientSession | None = None
|
|
48
|
+
|
|
49
|
+
async def __aenter__(self) -> "YuChatApiClient":
|
|
50
|
+
await self.start()
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
async def __aexit__(
|
|
54
|
+
self,
|
|
55
|
+
exc_type: type[BaseException] | None,
|
|
56
|
+
exc_value: BaseException | None,
|
|
57
|
+
traceback: TracebackType | None,
|
|
58
|
+
) -> None:
|
|
59
|
+
await self.close()
|
|
60
|
+
|
|
61
|
+
async def start(self) -> None:
|
|
62
|
+
if self._session is not None:
|
|
63
|
+
return
|
|
64
|
+
timeout = aiohttp.ClientTimeout(total=self._timeout_seconds)
|
|
65
|
+
connector = aiohttp.TCPConnector(limit=self._connection_limit)
|
|
66
|
+
self._session = aiohttp.ClientSession(
|
|
67
|
+
timeout=timeout,
|
|
68
|
+
connector=connector,
|
|
69
|
+
headers={
|
|
70
|
+
"Accept": "application/json",
|
|
71
|
+
"Authorization": f"Bearer {self._token}",
|
|
72
|
+
"Content-Type": "application/json",
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
async def close(self) -> None:
|
|
77
|
+
if self._session is None:
|
|
78
|
+
return
|
|
79
|
+
await self._session.close()
|
|
80
|
+
self._session = None
|
|
81
|
+
|
|
82
|
+
async def set_update_settings(self, request: SetUpdateSettingsRequestDto) -> None:
|
|
83
|
+
"""Сохраняет на стороне YuChat настройки получаемых событий."""
|
|
84
|
+
await self._post("/public/v2/setUpdateSettings", request=request, parse_json=False)
|
|
85
|
+
|
|
86
|
+
async def get_me(self) -> GetMeResponseDto:
|
|
87
|
+
"""Получает сведения о текущем боте."""
|
|
88
|
+
payload = await self._post("/public/v2/getMe")
|
|
89
|
+
return self._parse_response(payload, GetMeResponseDto)
|
|
90
|
+
|
|
91
|
+
async def get_updates(self, request: GetUpdatesRequestDto) -> GetUpdatesResponseDto:
|
|
92
|
+
"""Получает следующую пачку событий после указанного offset."""
|
|
93
|
+
payload = await self._post("/public/v2/getUpdates", request=request)
|
|
94
|
+
return self._parse_response(payload, GetUpdatesResponseDto)
|
|
95
|
+
|
|
96
|
+
async def send_message(self, request: SendMessageRequestDto) -> SendMessageResponseDto:
|
|
97
|
+
"""Отправляет сообщение в явно указанный чат."""
|
|
98
|
+
payload = await self._post("/public/v2/sendMessage", request=request)
|
|
99
|
+
return self._parse_response(payload, SendMessageResponseDto)
|
|
100
|
+
|
|
101
|
+
async def edit_message(self, request: EditMessageRequestDto) -> None:
|
|
102
|
+
"""Заменяет текст и кнопки существующего сообщения."""
|
|
103
|
+
await self._post("/public/v2/editMessage", request=request, parse_json=False)
|
|
104
|
+
|
|
105
|
+
async def _post(
|
|
106
|
+
self,
|
|
107
|
+
path: str,
|
|
108
|
+
request: BaseModel | None = None,
|
|
109
|
+
*,
|
|
110
|
+
parse_json: bool = True,
|
|
111
|
+
) -> object | None:
|
|
112
|
+
session = self._require_session()
|
|
113
|
+
url = f"{self._base_url}{path}"
|
|
114
|
+
payload = (
|
|
115
|
+
request.model_dump(mode="json", by_alias=True, exclude_none=True)
|
|
116
|
+
if request is not None
|
|
117
|
+
else None
|
|
118
|
+
)
|
|
119
|
+
try:
|
|
120
|
+
async with session.post(url, json=payload) as response:
|
|
121
|
+
self._raise_for_status(response.status, response.headers, path)
|
|
122
|
+
if not parse_json:
|
|
123
|
+
return None
|
|
124
|
+
try:
|
|
125
|
+
response_payload: object = await response.json(content_type=None)
|
|
126
|
+
return response_payload
|
|
127
|
+
except (aiohttp.ContentTypeError, json.JSONDecodeError, UnicodeDecodeError) as exc:
|
|
128
|
+
raise YuChatUnexpectedResponseError("YuChat вернул некорректный JSON") from exc
|
|
129
|
+
except (TimeoutError, aiohttp.ClientConnectionError) as exc:
|
|
130
|
+
raise YuChatTransientError("YuChat временно недоступен") from exc
|
|
131
|
+
except aiohttp.ClientError as exc:
|
|
132
|
+
raise YuChatClientError("Ошибка выполнения запроса к YuChat") from exc
|
|
133
|
+
|
|
134
|
+
def _require_session(self) -> aiohttp.ClientSession:
|
|
135
|
+
if self._session is None:
|
|
136
|
+
raise YuChatClientError("HTTP-клиент YuChat не запущен")
|
|
137
|
+
return self._session
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def _parse_response(payload: object | None, model_type: type[ModelType]) -> ModelType:
|
|
141
|
+
try:
|
|
142
|
+
return model_type.model_validate(payload)
|
|
143
|
+
except ValidationError as exc:
|
|
144
|
+
raise YuChatUnexpectedResponseError("Формат ответа YuChat не поддерживается") from exc
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def _raise_for_status(
|
|
148
|
+
status: int,
|
|
149
|
+
headers: Mapping[str, str],
|
|
150
|
+
path: str,
|
|
151
|
+
) -> None:
|
|
152
|
+
if 200 <= status < 300:
|
|
153
|
+
return
|
|
154
|
+
if status in AUTHORIZATION_ERROR_STATUSES:
|
|
155
|
+
raise YuChatAuthorizationError("YuChat отклонил токен или права бота")
|
|
156
|
+
if status == RATE_LIMIT_STATUS:
|
|
157
|
+
retry_after = headers.get("Retry-After")
|
|
158
|
+
raise YuChatRateLimitError(YuChatApiClient._parse_retry_after(retry_after))
|
|
159
|
+
if status >= SERVER_ERROR_STATUS:
|
|
160
|
+
raise YuChatTransientError(f"YuChat временно недоступен: status={status}, path={path}")
|
|
161
|
+
raise YuChatUnexpectedResponseError(
|
|
162
|
+
f"YuChat вернул неожиданный статус: status={status}, path={path}"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
@staticmethod
|
|
166
|
+
def _parse_retry_after(raw_value: str | None) -> float | None:
|
|
167
|
+
if raw_value is None:
|
|
168
|
+
return None
|
|
169
|
+
try:
|
|
170
|
+
return max(0.0, float(raw_value))
|
|
171
|
+
except ValueError:
|
|
172
|
+
return None
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Исключения Public API YuChat."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class YuChatClientError(RuntimeError):
|
|
5
|
+
"""Представляет базовую ошибку вызова YuChat без чувствительных данных."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class YuChatAuthorizationError(YuChatClientError):
|
|
9
|
+
"""Сообщает об отклонении токена или недостаточных правах бота."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class YuChatTransientError(YuChatClientError):
|
|
13
|
+
"""Сообщает о временной сетевой или серверной ошибке."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class YuChatUnexpectedResponseError(YuChatClientError):
|
|
17
|
+
"""Сообщает о неподдерживаемом статусе или формате ответа YuChat."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class YuChatRateLimitError(YuChatClientError):
|
|
21
|
+
"""Передаёт рекомендуемую задержку после ответа 429."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, retry_after_seconds: float | None) -> None:
|
|
24
|
+
super().__init__("YuChat временно ограничил частоту запросов")
|
|
25
|
+
self.retry_after_seconds = retry_after_seconds
|