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.
Files changed (31) hide show
  1. yuchatlib-0.1.0/.gitignore +12 -0
  2. yuchatlib-0.1.0/LICENSE +21 -0
  3. yuchatlib-0.1.0/PKG-INFO +87 -0
  4. yuchatlib-0.1.0/README.md +67 -0
  5. yuchatlib-0.1.0/pyproject.toml +51 -0
  6. yuchatlib-0.1.0/src/yuchatlib/__init__.py +109 -0
  7. yuchatlib-0.1.0/src/yuchatlib/api/__init__.py +1 -0
  8. yuchatlib-0.1.0/src/yuchatlib/api/client.py +172 -0
  9. yuchatlib-0.1.0/src/yuchatlib/api/errors.py +25 -0
  10. yuchatlib-0.1.0/src/yuchatlib/api/models.py +341 -0
  11. yuchatlib-0.1.0/src/yuchatlib/bot.py +122 -0
  12. yuchatlib-0.1.0/src/yuchatlib/dispatch/__init__.py +29 -0
  13. yuchatlib-0.1.0/src/yuchatlib/dispatch/command.py +42 -0
  14. yuchatlib-0.1.0/src/yuchatlib/dispatch/context.py +75 -0
  15. yuchatlib-0.1.0/src/yuchatlib/dispatch/dispatcher.py +42 -0
  16. yuchatlib-0.1.0/src/yuchatlib/dispatch/filters.py +79 -0
  17. yuchatlib-0.1.0/src/yuchatlib/dispatch/router.py +85 -0
  18. yuchatlib-0.1.0/src/yuchatlib/facade.py +192 -0
  19. yuchatlib-0.1.0/src/yuchatlib/fsm/__init__.py +18 -0
  20. yuchatlib-0.1.0/src/yuchatlib/fsm/context.py +31 -0
  21. yuchatlib-0.1.0/src/yuchatlib/fsm/memory.py +95 -0
  22. yuchatlib-0.1.0/src/yuchatlib/fsm/state.py +26 -0
  23. yuchatlib-0.1.0/src/yuchatlib/fsm/storage.py +47 -0
  24. yuchatlib-0.1.0/src/yuchatlib/polling/__init__.py +12 -0
  25. yuchatlib-0.1.0/src/yuchatlib/polling/config.py +31 -0
  26. yuchatlib-0.1.0/src/yuchatlib/polling/offset.py +36 -0
  27. yuchatlib-0.1.0/src/yuchatlib/polling/worker.py +217 -0
  28. yuchatlib-0.1.0/src/yuchatlib/py.typed +1 -0
  29. yuchatlib-0.1.0/tests/smoke/test_command_fsm_flow.py +79 -0
  30. yuchatlib-0.1.0/tests/smoke/test_polling_flow.py +78 -0
  31. yuchatlib-0.1.0/tests/smoke/test_public_api.py +47 -0
@@ -0,0 +1,12 @@
1
+ .venv/
2
+ .idea/
3
+ .mypy_cache/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ __pycache__/
7
+ *.py[cod]
8
+ *.egg-info/
9
+ build/
10
+ dist/
11
+ .env
12
+ .pypirc
@@ -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.
@@ -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