vkuxt 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.
- vkuxt-0.1.0/.gitignore +167 -0
- vkuxt-0.1.0/LICENSE +21 -0
- vkuxt-0.1.0/PKG-INFO +216 -0
- vkuxt-0.1.0/README.md +190 -0
- vkuxt-0.1.0/examples/__init__.py +0 -0
- vkuxt-0.1.0/examples/tictactoe.py +622 -0
- vkuxt-0.1.0/pyproject.toml +57 -0
- vkuxt-0.1.0/src/vkuxt/__init__.py +48 -0
- vkuxt-0.1.0/src/vkuxt/adapters/__init__.py +23 -0
- vkuxt-0.1.0/src/vkuxt/adapters/vk.py +861 -0
- vkuxt-0.1.0/src/vkuxt/app.py +842 -0
- vkuxt-0.1.0/src/vkuxt/codec.py +115 -0
- vkuxt-0.1.0/src/vkuxt/interfaces.py +89 -0
- vkuxt-0.1.0/src/vkuxt/locks.py +140 -0
- vkuxt-0.1.0/src/vkuxt/models.py +70 -0
- vkuxt-0.1.0/src/vkuxt/py.typed +0 -0
- vkuxt-0.1.0/src/vkuxt/storage.py +172 -0
vkuxt-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
### Python template
|
|
2
|
+
# Byte-compiled / optimized / DLL files
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.py[cod]
|
|
5
|
+
*$py.class
|
|
6
|
+
|
|
7
|
+
# C extensions
|
|
8
|
+
*.so
|
|
9
|
+
|
|
10
|
+
# Distribution / packaging
|
|
11
|
+
.Python
|
|
12
|
+
build/
|
|
13
|
+
develop-eggs/
|
|
14
|
+
dist/
|
|
15
|
+
downloads/
|
|
16
|
+
eggs/
|
|
17
|
+
.eggs/
|
|
18
|
+
lib/
|
|
19
|
+
lib64/
|
|
20
|
+
parts/
|
|
21
|
+
sdist/
|
|
22
|
+
var/
|
|
23
|
+
wheels/
|
|
24
|
+
share/python-wheels/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.installed.cfg
|
|
27
|
+
*.egg
|
|
28
|
+
MANIFEST
|
|
29
|
+
|
|
30
|
+
# PyInstaller
|
|
31
|
+
# Usually these files are written by a python script from a template
|
|
32
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
33
|
+
*.manifest
|
|
34
|
+
*.spec
|
|
35
|
+
|
|
36
|
+
# Installer logs
|
|
37
|
+
pip-log.txt
|
|
38
|
+
pip-delete-this-directory.txt
|
|
39
|
+
|
|
40
|
+
# Unit test / coverage reports
|
|
41
|
+
htmlcov/
|
|
42
|
+
.tox/
|
|
43
|
+
.nox/
|
|
44
|
+
.coverage
|
|
45
|
+
.coverage.*
|
|
46
|
+
.cache
|
|
47
|
+
nosetests.xml
|
|
48
|
+
coverage.xml
|
|
49
|
+
*.cover
|
|
50
|
+
*.py,cover
|
|
51
|
+
.hypothesis/
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
cover/
|
|
54
|
+
|
|
55
|
+
# Translations
|
|
56
|
+
*.mo
|
|
57
|
+
*.pot
|
|
58
|
+
|
|
59
|
+
# Django stuff:
|
|
60
|
+
*.log
|
|
61
|
+
local_settings.py
|
|
62
|
+
db.sqlite3
|
|
63
|
+
db.sqlite3-journal
|
|
64
|
+
|
|
65
|
+
# Flask stuff:
|
|
66
|
+
instance/
|
|
67
|
+
.webassets-cache
|
|
68
|
+
|
|
69
|
+
# Scrapy stuff:
|
|
70
|
+
.scrapy
|
|
71
|
+
|
|
72
|
+
# Sphinx documentation
|
|
73
|
+
docs/_build/
|
|
74
|
+
|
|
75
|
+
# PyBuilder
|
|
76
|
+
.pybuilder/
|
|
77
|
+
target/
|
|
78
|
+
|
|
79
|
+
# Jupyter Notebook
|
|
80
|
+
.ipynb_checkpoints
|
|
81
|
+
|
|
82
|
+
# IPython
|
|
83
|
+
profile_default/
|
|
84
|
+
ipython_config.py
|
|
85
|
+
|
|
86
|
+
# pyenv
|
|
87
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
88
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
89
|
+
# .python-version
|
|
90
|
+
|
|
91
|
+
# pipenv
|
|
92
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
93
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
94
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
95
|
+
# install all needed dependencies.
|
|
96
|
+
#Pipfile.lock
|
|
97
|
+
|
|
98
|
+
# poetry
|
|
99
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
100
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
101
|
+
# commonly ignored for libraries.
|
|
102
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
103
|
+
#poetry.lock
|
|
104
|
+
|
|
105
|
+
# pdm
|
|
106
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
107
|
+
#pdm.lock
|
|
108
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
109
|
+
# in version control.
|
|
110
|
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
111
|
+
.pdm.toml
|
|
112
|
+
.pdm-python
|
|
113
|
+
.pdm-build/
|
|
114
|
+
|
|
115
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
116
|
+
__pypackages__/
|
|
117
|
+
|
|
118
|
+
# Celery stuff
|
|
119
|
+
celerybeat-schedule
|
|
120
|
+
celerybeat.pid
|
|
121
|
+
|
|
122
|
+
# SageMath parsed files
|
|
123
|
+
*.sage.py
|
|
124
|
+
|
|
125
|
+
# Environments
|
|
126
|
+
.env
|
|
127
|
+
.venv
|
|
128
|
+
env/
|
|
129
|
+
venv/
|
|
130
|
+
ENV/
|
|
131
|
+
env.bak/
|
|
132
|
+
venv.bak/
|
|
133
|
+
|
|
134
|
+
# Spyder project settings
|
|
135
|
+
.spyderproject
|
|
136
|
+
.spyproject
|
|
137
|
+
|
|
138
|
+
# Rope project settings
|
|
139
|
+
.ropeproject
|
|
140
|
+
|
|
141
|
+
# mkdocs documentation
|
|
142
|
+
/site
|
|
143
|
+
|
|
144
|
+
# mypy
|
|
145
|
+
.mypy_cache/
|
|
146
|
+
.dmypy.json
|
|
147
|
+
dmypy.json
|
|
148
|
+
|
|
149
|
+
# Pyre type checker
|
|
150
|
+
.pyre/
|
|
151
|
+
|
|
152
|
+
# pytype static type analyzer
|
|
153
|
+
.pytype/
|
|
154
|
+
|
|
155
|
+
# Cython debug symbols
|
|
156
|
+
cython_debug/
|
|
157
|
+
|
|
158
|
+
# PyCharm
|
|
159
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
160
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
161
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
162
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
163
|
+
.idea/
|
|
164
|
+
/*.py
|
|
165
|
+
/uv.lock
|
|
166
|
+
AGENTS.md
|
|
167
|
+
release.ps1
|
vkuxt-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 to4e4ka
|
|
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.
|
vkuxt-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: vkuxt
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Асинхронный каркас для приложений и ботов VK
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: asyncio,bot,callback-api,vk,vk-api
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Framework :: AsyncIO
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.13
|
|
17
|
+
Requires-Dist: matrix-planner>=0.2.1
|
|
18
|
+
Requires-Dist: pydantic>=2.13.5
|
|
19
|
+
Requires-Dist: vkx[http]>=0.1.12
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: fastapi>=0.110; extra == 'dev'
|
|
22
|
+
Requires-Dist: pyright>=1.1; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# vkuxt
|
|
28
|
+
|
|
29
|
+
`vkuxt` — асинхронный каркас для приложений, в которых пользовательские действия приходят через адаптеры, например VK-бота. Он не навязывает FSM, схему бизнес-состояния, шаблонизатор, рендеринг или правила перерисовки. Транспорт и типизированные VK API-вызовы предоставляет `vkx`.
|
|
30
|
+
|
|
31
|
+
## Установка
|
|
32
|
+
|
|
33
|
+
Нужен Python 3.13+.
|
|
34
|
+
|
|
35
|
+
```powershell
|
|
36
|
+
uv sync --extra dev # разработка в репозитории
|
|
37
|
+
uv add vkuxt # в приложении, когда пакет опубликован
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`vkx` входит в основные зависимости; отдельный extra для VK не требуется.
|
|
41
|
+
|
|
42
|
+
## Модель
|
|
43
|
+
|
|
44
|
+
- `App` передаёт сырой ввод адаптерам по очереди. Первый распознавший формат адаптер возвращает внутренний `Input` с полями `metadata`, `data`, `text` и `interface`; текст и typed payload могут присутствовать одновременно. Прикладной хендлер получает уже не `Input`.
|
|
45
|
+
- Контракт хендлера: `(ctx, metadata, data, text)`. `ctx` — `Context`, привязанный к текущему интерфейсу; `metadata` — экземпляр `BaseMetadata`, `data` — валидированная модель `InputData` или `None`, `text` — строка (пустая, если текста нет). В аннотациях metadata и data указывается одна модель или `Union` моделей через `|`. Диспетчер пропускает хендлер, если metadata не подходит по типу или payload не валидируется ни в одну из указанных data-моделей. `data: None` означает событие без payload; `data: Model | None` принимает оба варианта. `BaseMetadata` и `InputData` в аннотации работают как широкие фильтры.
|
|
46
|
+
- `ctx.state` — изменяемый Pydantic-namespace `StateNamespace` с доступом по атрибутам, привязанный к текущему интерфейсу. После успешного завершения хендлера он сохраняется автоматически. `ctx.redraw(view)` перерисовывает текущий интерфейс и переносит состояние на новый handle; `ctx.redraw(None)` удаляет интерфейс и его состояние. Через `ctx` доступны `ctx.app`, `ctx.adapter`, `ctx.interface` и `ctx.metadata`. Параметризуйте `Context` типом адаптера только там, где нужен его специфичный API (`Context[VKMessagesCallbackAdapter]`); второй параметр (`Context[VKMessagesCallbackAdapter, ScreenState]`) задаёт схему состояния для IDE и валидации.
|
|
47
|
+
- По умолчанию хендлеры fire-and-forget: `App.dispatch_event` только запускает их в одной `asyncio.Task` и сразу возвращает управление. `await app.wait_background()` ждёт завершения; `App.close()` также дренирует задачи. `@app.handler(background=False)` оставляет явный foreground-режим: такие хендлеры вызываются последовательно, а первый непустой результат возвращается адаптеру. `raise StopHandlerChain` прекращает текущую фазу опроса.
|
|
48
|
+
- `InputData` и `StateNamespace` проверяют строгую JSON-сериализуемость при Pydantic-валидации и присваивании. `handler_name` — обычное поле данных; диспетчер не интерпретирует его. Чтобы не передавать `handler_name` вручную, задайте значение по умолчанию прямо в модели: `handler_name: Literal["ttt.close"] = "ttt.close"`.
|
|
49
|
+
- Ключи приложения — обычные словари, dataclass'ы или Pydantic-модели. `encode_key` канонически сериализует их; данные сортируются по ключам, а тип по умолчанию кодируется полным именем класса. Для стабильного тега можно объявить `__vkuxt_key_type__: ClassVar[str]`, чтобы перенос модуля не ломал сохранённые ключи. Storage-ключи версионированы (`vkuxt:v1:...`); ключи предыдущих alpha-версий без `v1` не читаются.
|
|
50
|
+
- Аннотации хендлера разрешаются при регистрации. Неразрешимое имя, неверное число аргументов, `ctx` не `Context`, второй параметр `Context` не из `StateNamespace`, metadata не из `BaseMetadata`, data не из `InputData`/`None` или `text` не `str` приводят к `TypeError`, а не к тихому превращению хендлера в wildcard. Ошибки фоновых хендлеров и запуска адаптеров пишутся в `logging.getLogger("vkuxt")`.
|
|
51
|
+
- `Interface` — общий контракт отрисовываемого экрана: `text` и двумерный список кнопок. Адаптер объявляет свой подкласс через `Adapter.interface_type`; для VK это `VKInterface`, который проверяет лимиты сообщения и inline-клавиатуры.
|
|
52
|
+
- Конкретный удалённый интерфейс представлен `InterfaceHandle` — Python-объектом с непрозрачным `id`, который не меняется при перерисовках. Прикладной код передаёт handle в `Adapter.redraw()` и не работает с `cmid`/peer-id напрямую. Входящий интерфейс доступен хендлеру как `ctx.interface`; `ctx.redraw(view)` рисует именно его.
|
|
53
|
+
- Callback-payload'ы кладутся в кнопки обычными `InputData` или словарями. Адаптер сам сериализует их и заменяет payload длиннее 255 байт UTF-8 коротким token; token'ы хранятся в записи интерфейса. `App(tokenize_long_input=False)` отключает замену.
|
|
54
|
+
- Рендеринг остаётся ответственностью приложения: оно собирает `Interface` и вызывает `ctx.redraw(view)`, а адаптер решает, отправить сообщение, отредактировать существующее или удалить его.
|
|
55
|
+
- Handler может явно блокировать одно или несколько состояний через `async with app.lock(key, ...)`. Lock удерживается во время тела блока; читайте состояние после получения lock. Ключи блокируются в каноническом порядке, повторное получение уже удерживаемого набора допустимо. Новые ключи при вложенном захвате должны сортироваться позже всех уже удерживаемых; иначе выбрасывается `RuntimeError`, потому что такой захват может привести к взаимоблокировке. Блокировки процессные и рассчитаны на один event loop/процесс.
|
|
56
|
+
- Изменение состояния само по себе не инициирует перерисовку. Handler собирает `Interface` и вызывает `ctx.redraw()`.
|
|
57
|
+
|
|
58
|
+
## Минимальный пример
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from typing import Literal
|
|
62
|
+
|
|
63
|
+
from vkuxt import (
|
|
64
|
+
App,
|
|
65
|
+
Context,
|
|
66
|
+
InputData,
|
|
67
|
+
MemoryStorage,
|
|
68
|
+
VKInterface,
|
|
69
|
+
VKMessageMetadata,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
app = App(storage=MemoryStorage())
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Greeting(InputData):
|
|
76
|
+
handler_name: Literal["greeting-data"] = "greeting-data"
|
|
77
|
+
text: str
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@app.handler
|
|
81
|
+
async def greeting(
|
|
82
|
+
ctx: Context,
|
|
83
|
+
metadata: VKMessageMetadata,
|
|
84
|
+
data: Greeting | None,
|
|
85
|
+
text: str,
|
|
86
|
+
) -> None:
|
|
87
|
+
if data is not None:
|
|
88
|
+
message = data.text
|
|
89
|
+
elif text.casefold() == "привет":
|
|
90
|
+
message = text
|
|
91
|
+
else:
|
|
92
|
+
return
|
|
93
|
+
ctx.state.last = message
|
|
94
|
+
await ctx.redraw(
|
|
95
|
+
VKInterface(
|
|
96
|
+
text=f"{message}, {metadata.user_id}",
|
|
97
|
+
kb=[[VKInterface.callback("Ещё", Greeting(text="кнопка"))]],
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
До вызова хендлера `App` проверяет payload в `Greeting`; если он невалиден, хендлер пропускается. `None` в типе data позволяет тому же хендлеру получать текстовые события без payload. Можно объявить несколько моделей и типов metadata, например `data: StartData | StopData`, `metadata: VKMessageMetadata | OtherMetadata`. `handler_name` остаётся обычным полем данных и не интерпретируется диспетчером; значение по умолчанию избавляет от повторения в каждом конструкторе.
|
|
103
|
+
|
|
104
|
+
`ctx.state` автоматически сохраняется после хендлера. `ctx.redraw(view)` перерисовывает текущий интерфейс; вручную регистрировать внешний ID не нужно. `VKInterface.callback` принимает `InputData` или словарь и сериализует payload:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
view = VKInterface(
|
|
108
|
+
text="Выберите действие:",
|
|
109
|
+
rows=[[
|
|
110
|
+
VKInterface.callback(
|
|
111
|
+
"Привет",
|
|
112
|
+
Greeting(text="кнопка"),
|
|
113
|
+
)
|
|
114
|
+
]],
|
|
115
|
+
)
|
|
116
|
+
await ctx.redraw(view) # текущий интерфейс события
|
|
117
|
+
await adapter.redraw(view, peer_id=123) # последний интерфейс диалога
|
|
118
|
+
await adapter.redraw(view, target=handle) # конкретный интерфейс
|
|
119
|
+
await ctx.redraw(None) # удалить текущий
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`VKInterface` можно собрать один раз на уровне модуля и переиспользовать: адаптер не изменяет объект при отправке. `target` выбирает конкретный существующий интерфейс, `peer_id` — последний интерфейс диалога.
|
|
123
|
+
|
|
124
|
+
`App.handle_input(raw)` передаёт запрос адаптерам по очереди и возвращает результат foreground-хендлера; при штатном fire-and-forget режиме возвращается `None`. VK-адаптер строит внутренний `Input` с `VKMessageMetadata`, базовым `InputData`, текстом и интерфейсом; одно событие может содержать текст и payload одновременно. В `metadata` доступны поля VK, а для `message_new` также исходная модель `vkx.models.Message` в `msg` (`None` для callback-кнопки). Текущий интерфейс события доступен как `ctx.interface`.
|
|
125
|
+
|
|
126
|
+
## Хранилища и TTL
|
|
127
|
+
|
|
128
|
+
`StateStorage` — абстрактный родительский класс async-хранилищ строковых ключей/значений, TTL и `cleanup_expired()`. В комплекте:
|
|
129
|
+
|
|
130
|
+
- `MemoryStorage` — для тестов и локальной разработки;
|
|
131
|
+
- `JSONFileStorage(path)` — однопроцессное файловое хранение с атомарной заменой файла. Запись выполняется в отдельном потоке (`asyncio.to_thread`) и не блокирует event loop.
|
|
132
|
+
|
|
133
|
+
Redis-бэкенд намеренно не входит в библиотеку: прикладной слой может реализовать `StateStorage` под свой клиент. Pydantic-модели состояния сохраняются как JSON; тип модели передаётся в `get_state(key, Model)`. Обычные dict/list/scalars можно читать без модели, для словаря явно передайте `dict`. `App(ttl=...)` задаёт TTL по умолчанию; у отдельного интерфейса он переопределяется через `VKInterface(ttl=...)` или `adapter.new_interface(ttl=...)`. `None` означает без срока жизни. `matrix-planner` периодически вызывает обязательный `cleanup_expired()`.
|
|
134
|
+
|
|
135
|
+
`ctx.state` — изменяемый Pydantic-namespace `StateNamespace`, а не словарь. Поля читаются и пишутся как атрибуты, объявленные в подклассе поля видны IDE и валидируются при присваивании, а необъявленные атрибуты разрешены и сохраняются как extra-поля. Значения обязаны сериализоваться в строгий JSON; проверка выполняется при каждом присваивании. Состояние автоматически сохраняется после успешного хендлера. При наличии интерфейса оно хранится под стабильным `InterfaceHandle.state_key()`; при событии без интерфейса используется ключ `Adapter.context_key(metadata)`. `ctx.redraw(view)` переносит состояние на новый handle, а `ctx.redraw(None)` удаляет его вместе с интерфейсом. Для общего состояния, разделяемого несколькими интерфейсами (например, партии на двух досках), по-прежнему используйте `app.get_state`/`app.set_state`; это необязательное удобство, прикладной слой может хранить такие данные где угодно. Сам handle как ключ использовать нельзя: его `actions` и `ttl` меняются. Внутренние записи интерфейсов vkuxt хранит под непрозрачными ID адаптера, чтобы переживать перерисовки и длинные callback-payload'ы.
|
|
136
|
+
|
|
137
|
+
Схему состояния можно объявить вторым параметром `Context`:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from vkuxt import Context, StateNamespace
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class ScreenState(StateNamespace):
|
|
144
|
+
uid: int = 0
|
|
145
|
+
game_id: str = ""
|
|
146
|
+
seat: int = 0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@app.handler
|
|
150
|
+
async def show_game(
|
|
151
|
+
ctx: Context[VKMessagesCallbackAdapter, ScreenState],
|
|
152
|
+
metadata: VKMessageMetadata,
|
|
153
|
+
data: None,
|
|
154
|
+
text: str,
|
|
155
|
+
) -> None:
|
|
156
|
+
ctx.state.uid = metadata.user_id
|
|
157
|
+
ctx.state.extra_value = "сохранится как extra-поле"
|
|
158
|
+
await ctx.redraw(view)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Второй параметр необязателен: голый `Context` использует базовый `StateNamespace`, где доступны любые атрибуты, но IDE не знает их имён. Поля типизированного namespace должны иметь значения по умолчанию, чтобы состояние можно было создать при первом событии. Если сохранённого JSON нет или он не соответствует модели, `App` создаёт namespace из значений по умолчанию либо сообщает об ошибке загрузки.
|
|
162
|
+
|
|
163
|
+
Бизнес-состояние можно хранить Pydantic-моделью, а не словарём:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
class GameState(BaseModel):
|
|
167
|
+
status: Literal["active", "finished"]
|
|
168
|
+
game_id: str
|
|
169
|
+
board: list[str | None]
|
|
170
|
+
turn: int
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
async with ctx.app.lock(game_key):
|
|
174
|
+
game = await ctx.app.get_state(game_key, GameState)
|
|
175
|
+
if game is None:
|
|
176
|
+
return
|
|
177
|
+
game.turn = 1 - game.turn
|
|
178
|
+
await ctx.app.set_state(game_key, game, ttl=GAME_TTL)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`set_state` сериализует модель в JSON, `get_state(key, GameState)` валидирует её обратно. С `JSONFileStorage` состояние переживает перезапуск приложения и снова приходит типизированным объектом; отдельного общего `app.state`-словаря нет, потому что ключ и TTL задаются явно.
|
|
182
|
+
|
|
183
|
+
`JSONFileStorage` рассчитан на один процесс и один экземпляр на файл: параллельная запись из нескольких процессов или экземпляров не поддерживается. Повреждённый JSON при старте приводит к `RuntimeError` с путём файла, а не к тихой перезаписи.
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from vkuxt import App, JSONFileStorage
|
|
187
|
+
|
|
188
|
+
app = App(JSONFileStorage("state.json"), ttl=60 * 60 * 24)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
При передаче внешнего `Planner` его жизненным циклом управляет приложение-хозяин; `App.close()` снимает только задачу очистки этого приложения. `App(close_timeout=...)` ограничивает ожидание фоновых хендлеров при остановке (по умолчанию 10 секунд); по истечении они отменяются. `App.start()` и `App.close()` сериализованы, повторный `close()` дожидается уже идущей остановки. `await app.wait_background()` ждёт завершения фоновых хендлеров, `await app.wait_adapters_started()` — запуска адаптеров, добавленных во время работы. Контейнер `app.services` свободен для сервисов приложения.
|
|
192
|
+
|
|
193
|
+
## VK Callback API
|
|
194
|
+
|
|
195
|
+
`VKCallbackAdapter` и `VKMessagesAdapter` — абстрактные слои для Callback API и VK Messages. Готовый вариант объединяет их: `VKMessagesCallbackAdapter`. Адаптер создаётся вручную и передаётся готовым инстансом в `app.add_adapter(adapter)`; `App.start()` запускает зарегистрированные адаптеры, а новые адаптеры, добавленные во время работы, запускаются асинхронно в текущем event loop. `App.close()` останавливает запущенные адаптеры в обратном порядке. Если `messages.edit` завершился ошибкой, `redraw()` логирует её и отправляет новое сообщение вместо падения webhook'а.
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
adapter = app.add_adapter(VKMessagesCallbackAdapter(app, vk, group_id=group_id))
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Вложения принимают типы VKX или VK-строки, кнопки — модели `vkx.KeyboardButton`. `VKInterface.callback(..., color=...)` принимает `KeyboardButtonColor` или строку и проверяет допустимые значения. `KeyboardButtonColor` реэкспортируется из `vkuxt` и `vkuxt.adapters.vk`, поэтому отдельный импорт из `vkx.models` не нужен.
|
|
202
|
+
|
|
203
|
+
`await ctx.redraw(view)` выбирает интерфейс текущего события: редактирует сообщение callback-кнопки, отправляет ответ на текст и удаляет старый интерфейс либо отправляет новое сообщение. Для чужого или явно выбранного интерфейса используйте `await adapter.redraw(view, target=handle)`; `peer_id=...` выбирает последний интерфейс диалога. `await ctx.redraw(None)` удаляет текущий интерфейс. `await adapter.send(peer_id, view)` и `await adapter.edit(handle, view)` доступны для явных операций.
|
|
204
|
+
|
|
205
|
+
`App.lifespan` можно передать напрямую в Starlette/FastAPI, чтобы запускать и останавливать адаптеры вместе с ASGI-приложением:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
web = FastAPI(lifespan=app.lifespan)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
VK Callback адаптер использует `vkx.server.CallbackServer` для проверки входа и разбора уведомлений в модели `vkx`. Дедупликация доставленных событий — ответственность `vkx`: `CallbackServer` отбрасывает повторный `event_id` и повторную доставку `message_new` с той же парой `peer_id` + `conversation_message_id`. У `message_event` идентификатор лежит внутри `object.event_id`, поэтому адаптер дополнительно передаёт его в `CallbackServer.is_duplicate`. Текст приходит VK-хендлеру аргументом `text`; адаптер сам определяет, было ли сообщение ответом на сохранённый интерфейс. В личных диалогах можно включить определение последнего активного интерфейса. Если foreground-хендлер не привёл к edit/delete именно нажатого интерфейса, адаптер отправляет `messages.sendMessageEventAnswer`. Индекс последних интерфейсов peer'а ограничен 10 записями и получает TTL интерфейса.
|
|
212
|
+
|
|
213
|
+
При fire-and-forget обработке `sendMessageEventAnswer` отправляется сразу после запуска фоновой задачи, не дожидаясь её завершения. Это укладывается в окно ответа VK; если нужен результат хендлера до ответа, используйте `background=False`, но держите такой путь коротким.
|
|
214
|
+
|
|
215
|
+
Полный скетч крестиков-ноликов, включая лобби с кодом, ввод кода ответом на сообщение, две синхронизируемые доски и сдачу, находится в [`examples/tictactoe.py`](examples/tictactoe.py).
|
|
216
|
+
|
vkuxt-0.1.0/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# vkuxt
|
|
2
|
+
|
|
3
|
+
`vkuxt` — асинхронный каркас для приложений, в которых пользовательские действия приходят через адаптеры, например VK-бота. Он не навязывает FSM, схему бизнес-состояния, шаблонизатор, рендеринг или правила перерисовки. Транспорт и типизированные VK API-вызовы предоставляет `vkx`.
|
|
4
|
+
|
|
5
|
+
## Установка
|
|
6
|
+
|
|
7
|
+
Нужен Python 3.13+.
|
|
8
|
+
|
|
9
|
+
```powershell
|
|
10
|
+
uv sync --extra dev # разработка в репозитории
|
|
11
|
+
uv add vkuxt # в приложении, когда пакет опубликован
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`vkx` входит в основные зависимости; отдельный extra для VK не требуется.
|
|
15
|
+
|
|
16
|
+
## Модель
|
|
17
|
+
|
|
18
|
+
- `App` передаёт сырой ввод адаптерам по очереди. Первый распознавший формат адаптер возвращает внутренний `Input` с полями `metadata`, `data`, `text` и `interface`; текст и typed payload могут присутствовать одновременно. Прикладной хендлер получает уже не `Input`.
|
|
19
|
+
- Контракт хендлера: `(ctx, metadata, data, text)`. `ctx` — `Context`, привязанный к текущему интерфейсу; `metadata` — экземпляр `BaseMetadata`, `data` — валидированная модель `InputData` или `None`, `text` — строка (пустая, если текста нет). В аннотациях metadata и data указывается одна модель или `Union` моделей через `|`. Диспетчер пропускает хендлер, если metadata не подходит по типу или payload не валидируется ни в одну из указанных data-моделей. `data: None` означает событие без payload; `data: Model | None` принимает оба варианта. `BaseMetadata` и `InputData` в аннотации работают как широкие фильтры.
|
|
20
|
+
- `ctx.state` — изменяемый Pydantic-namespace `StateNamespace` с доступом по атрибутам, привязанный к текущему интерфейсу. После успешного завершения хендлера он сохраняется автоматически. `ctx.redraw(view)` перерисовывает текущий интерфейс и переносит состояние на новый handle; `ctx.redraw(None)` удаляет интерфейс и его состояние. Через `ctx` доступны `ctx.app`, `ctx.adapter`, `ctx.interface` и `ctx.metadata`. Параметризуйте `Context` типом адаптера только там, где нужен его специфичный API (`Context[VKMessagesCallbackAdapter]`); второй параметр (`Context[VKMessagesCallbackAdapter, ScreenState]`) задаёт схему состояния для IDE и валидации.
|
|
21
|
+
- По умолчанию хендлеры fire-and-forget: `App.dispatch_event` только запускает их в одной `asyncio.Task` и сразу возвращает управление. `await app.wait_background()` ждёт завершения; `App.close()` также дренирует задачи. `@app.handler(background=False)` оставляет явный foreground-режим: такие хендлеры вызываются последовательно, а первый непустой результат возвращается адаптеру. `raise StopHandlerChain` прекращает текущую фазу опроса.
|
|
22
|
+
- `InputData` и `StateNamespace` проверяют строгую JSON-сериализуемость при Pydantic-валидации и присваивании. `handler_name` — обычное поле данных; диспетчер не интерпретирует его. Чтобы не передавать `handler_name` вручную, задайте значение по умолчанию прямо в модели: `handler_name: Literal["ttt.close"] = "ttt.close"`.
|
|
23
|
+
- Ключи приложения — обычные словари, dataclass'ы или Pydantic-модели. `encode_key` канонически сериализует их; данные сортируются по ключам, а тип по умолчанию кодируется полным именем класса. Для стабильного тега можно объявить `__vkuxt_key_type__: ClassVar[str]`, чтобы перенос модуля не ломал сохранённые ключи. Storage-ключи версионированы (`vkuxt:v1:...`); ключи предыдущих alpha-версий без `v1` не читаются.
|
|
24
|
+
- Аннотации хендлера разрешаются при регистрации. Неразрешимое имя, неверное число аргументов, `ctx` не `Context`, второй параметр `Context` не из `StateNamespace`, metadata не из `BaseMetadata`, data не из `InputData`/`None` или `text` не `str` приводят к `TypeError`, а не к тихому превращению хендлера в wildcard. Ошибки фоновых хендлеров и запуска адаптеров пишутся в `logging.getLogger("vkuxt")`.
|
|
25
|
+
- `Interface` — общий контракт отрисовываемого экрана: `text` и двумерный список кнопок. Адаптер объявляет свой подкласс через `Adapter.interface_type`; для VK это `VKInterface`, который проверяет лимиты сообщения и inline-клавиатуры.
|
|
26
|
+
- Конкретный удалённый интерфейс представлен `InterfaceHandle` — Python-объектом с непрозрачным `id`, который не меняется при перерисовках. Прикладной код передаёт handle в `Adapter.redraw()` и не работает с `cmid`/peer-id напрямую. Входящий интерфейс доступен хендлеру как `ctx.interface`; `ctx.redraw(view)` рисует именно его.
|
|
27
|
+
- Callback-payload'ы кладутся в кнопки обычными `InputData` или словарями. Адаптер сам сериализует их и заменяет payload длиннее 255 байт UTF-8 коротким token; token'ы хранятся в записи интерфейса. `App(tokenize_long_input=False)` отключает замену.
|
|
28
|
+
- Рендеринг остаётся ответственностью приложения: оно собирает `Interface` и вызывает `ctx.redraw(view)`, а адаптер решает, отправить сообщение, отредактировать существующее или удалить его.
|
|
29
|
+
- Handler может явно блокировать одно или несколько состояний через `async with app.lock(key, ...)`. Lock удерживается во время тела блока; читайте состояние после получения lock. Ключи блокируются в каноническом порядке, повторное получение уже удерживаемого набора допустимо. Новые ключи при вложенном захвате должны сортироваться позже всех уже удерживаемых; иначе выбрасывается `RuntimeError`, потому что такой захват может привести к взаимоблокировке. Блокировки процессные и рассчитаны на один event loop/процесс.
|
|
30
|
+
- Изменение состояния само по себе не инициирует перерисовку. Handler собирает `Interface` и вызывает `ctx.redraw()`.
|
|
31
|
+
|
|
32
|
+
## Минимальный пример
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from typing import Literal
|
|
36
|
+
|
|
37
|
+
from vkuxt import (
|
|
38
|
+
App,
|
|
39
|
+
Context,
|
|
40
|
+
InputData,
|
|
41
|
+
MemoryStorage,
|
|
42
|
+
VKInterface,
|
|
43
|
+
VKMessageMetadata,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
app = App(storage=MemoryStorage())
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Greeting(InputData):
|
|
50
|
+
handler_name: Literal["greeting-data"] = "greeting-data"
|
|
51
|
+
text: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@app.handler
|
|
55
|
+
async def greeting(
|
|
56
|
+
ctx: Context,
|
|
57
|
+
metadata: VKMessageMetadata,
|
|
58
|
+
data: Greeting | None,
|
|
59
|
+
text: str,
|
|
60
|
+
) -> None:
|
|
61
|
+
if data is not None:
|
|
62
|
+
message = data.text
|
|
63
|
+
elif text.casefold() == "привет":
|
|
64
|
+
message = text
|
|
65
|
+
else:
|
|
66
|
+
return
|
|
67
|
+
ctx.state.last = message
|
|
68
|
+
await ctx.redraw(
|
|
69
|
+
VKInterface(
|
|
70
|
+
text=f"{message}, {metadata.user_id}",
|
|
71
|
+
kb=[[VKInterface.callback("Ещё", Greeting(text="кнопка"))]],
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
До вызова хендлера `App` проверяет payload в `Greeting`; если он невалиден, хендлер пропускается. `None` в типе data позволяет тому же хендлеру получать текстовые события без payload. Можно объявить несколько моделей и типов metadata, например `data: StartData | StopData`, `metadata: VKMessageMetadata | OtherMetadata`. `handler_name` остаётся обычным полем данных и не интерпретируется диспетчером; значение по умолчанию избавляет от повторения в каждом конструкторе.
|
|
77
|
+
|
|
78
|
+
`ctx.state` автоматически сохраняется после хендлера. `ctx.redraw(view)` перерисовывает текущий интерфейс; вручную регистрировать внешний ID не нужно. `VKInterface.callback` принимает `InputData` или словарь и сериализует payload:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
view = VKInterface(
|
|
82
|
+
text="Выберите действие:",
|
|
83
|
+
rows=[[
|
|
84
|
+
VKInterface.callback(
|
|
85
|
+
"Привет",
|
|
86
|
+
Greeting(text="кнопка"),
|
|
87
|
+
)
|
|
88
|
+
]],
|
|
89
|
+
)
|
|
90
|
+
await ctx.redraw(view) # текущий интерфейс события
|
|
91
|
+
await adapter.redraw(view, peer_id=123) # последний интерфейс диалога
|
|
92
|
+
await adapter.redraw(view, target=handle) # конкретный интерфейс
|
|
93
|
+
await ctx.redraw(None) # удалить текущий
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`VKInterface` можно собрать один раз на уровне модуля и переиспользовать: адаптер не изменяет объект при отправке. `target` выбирает конкретный существующий интерфейс, `peer_id` — последний интерфейс диалога.
|
|
97
|
+
|
|
98
|
+
`App.handle_input(raw)` передаёт запрос адаптерам по очереди и возвращает результат foreground-хендлера; при штатном fire-and-forget режиме возвращается `None`. VK-адаптер строит внутренний `Input` с `VKMessageMetadata`, базовым `InputData`, текстом и интерфейсом; одно событие может содержать текст и payload одновременно. В `metadata` доступны поля VK, а для `message_new` также исходная модель `vkx.models.Message` в `msg` (`None` для callback-кнопки). Текущий интерфейс события доступен как `ctx.interface`.
|
|
99
|
+
|
|
100
|
+
## Хранилища и TTL
|
|
101
|
+
|
|
102
|
+
`StateStorage` — абстрактный родительский класс async-хранилищ строковых ключей/значений, TTL и `cleanup_expired()`. В комплекте:
|
|
103
|
+
|
|
104
|
+
- `MemoryStorage` — для тестов и локальной разработки;
|
|
105
|
+
- `JSONFileStorage(path)` — однопроцессное файловое хранение с атомарной заменой файла. Запись выполняется в отдельном потоке (`asyncio.to_thread`) и не блокирует event loop.
|
|
106
|
+
|
|
107
|
+
Redis-бэкенд намеренно не входит в библиотеку: прикладной слой может реализовать `StateStorage` под свой клиент. Pydantic-модели состояния сохраняются как JSON; тип модели передаётся в `get_state(key, Model)`. Обычные dict/list/scalars можно читать без модели, для словаря явно передайте `dict`. `App(ttl=...)` задаёт TTL по умолчанию; у отдельного интерфейса он переопределяется через `VKInterface(ttl=...)` или `adapter.new_interface(ttl=...)`. `None` означает без срока жизни. `matrix-planner` периодически вызывает обязательный `cleanup_expired()`.
|
|
108
|
+
|
|
109
|
+
`ctx.state` — изменяемый Pydantic-namespace `StateNamespace`, а не словарь. Поля читаются и пишутся как атрибуты, объявленные в подклассе поля видны IDE и валидируются при присваивании, а необъявленные атрибуты разрешены и сохраняются как extra-поля. Значения обязаны сериализоваться в строгий JSON; проверка выполняется при каждом присваивании. Состояние автоматически сохраняется после успешного хендлера. При наличии интерфейса оно хранится под стабильным `InterfaceHandle.state_key()`; при событии без интерфейса используется ключ `Adapter.context_key(metadata)`. `ctx.redraw(view)` переносит состояние на новый handle, а `ctx.redraw(None)` удаляет его вместе с интерфейсом. Для общего состояния, разделяемого несколькими интерфейсами (например, партии на двух досках), по-прежнему используйте `app.get_state`/`app.set_state`; это необязательное удобство, прикладной слой может хранить такие данные где угодно. Сам handle как ключ использовать нельзя: его `actions` и `ttl` меняются. Внутренние записи интерфейсов vkuxt хранит под непрозрачными ID адаптера, чтобы переживать перерисовки и длинные callback-payload'ы.
|
|
110
|
+
|
|
111
|
+
Схему состояния можно объявить вторым параметром `Context`:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from vkuxt import Context, StateNamespace
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class ScreenState(StateNamespace):
|
|
118
|
+
uid: int = 0
|
|
119
|
+
game_id: str = ""
|
|
120
|
+
seat: int = 0
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@app.handler
|
|
124
|
+
async def show_game(
|
|
125
|
+
ctx: Context[VKMessagesCallbackAdapter, ScreenState],
|
|
126
|
+
metadata: VKMessageMetadata,
|
|
127
|
+
data: None,
|
|
128
|
+
text: str,
|
|
129
|
+
) -> None:
|
|
130
|
+
ctx.state.uid = metadata.user_id
|
|
131
|
+
ctx.state.extra_value = "сохранится как extra-поле"
|
|
132
|
+
await ctx.redraw(view)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Второй параметр необязателен: голый `Context` использует базовый `StateNamespace`, где доступны любые атрибуты, но IDE не знает их имён. Поля типизированного namespace должны иметь значения по умолчанию, чтобы состояние можно было создать при первом событии. Если сохранённого JSON нет или он не соответствует модели, `App` создаёт namespace из значений по умолчанию либо сообщает об ошибке загрузки.
|
|
136
|
+
|
|
137
|
+
Бизнес-состояние можно хранить Pydantic-моделью, а не словарём:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
class GameState(BaseModel):
|
|
141
|
+
status: Literal["active", "finished"]
|
|
142
|
+
game_id: str
|
|
143
|
+
board: list[str | None]
|
|
144
|
+
turn: int
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
async with ctx.app.lock(game_key):
|
|
148
|
+
game = await ctx.app.get_state(game_key, GameState)
|
|
149
|
+
if game is None:
|
|
150
|
+
return
|
|
151
|
+
game.turn = 1 - game.turn
|
|
152
|
+
await ctx.app.set_state(game_key, game, ttl=GAME_TTL)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`set_state` сериализует модель в JSON, `get_state(key, GameState)` валидирует её обратно. С `JSONFileStorage` состояние переживает перезапуск приложения и снова приходит типизированным объектом; отдельного общего `app.state`-словаря нет, потому что ключ и TTL задаются явно.
|
|
156
|
+
|
|
157
|
+
`JSONFileStorage` рассчитан на один процесс и один экземпляр на файл: параллельная запись из нескольких процессов или экземпляров не поддерживается. Повреждённый JSON при старте приводит к `RuntimeError` с путём файла, а не к тихой перезаписи.
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from vkuxt import App, JSONFileStorage
|
|
161
|
+
|
|
162
|
+
app = App(JSONFileStorage("state.json"), ttl=60 * 60 * 24)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
При передаче внешнего `Planner` его жизненным циклом управляет приложение-хозяин; `App.close()` снимает только задачу очистки этого приложения. `App(close_timeout=...)` ограничивает ожидание фоновых хендлеров при остановке (по умолчанию 10 секунд); по истечении они отменяются. `App.start()` и `App.close()` сериализованы, повторный `close()` дожидается уже идущей остановки. `await app.wait_background()` ждёт завершения фоновых хендлеров, `await app.wait_adapters_started()` — запуска адаптеров, добавленных во время работы. Контейнер `app.services` свободен для сервисов приложения.
|
|
166
|
+
|
|
167
|
+
## VK Callback API
|
|
168
|
+
|
|
169
|
+
`VKCallbackAdapter` и `VKMessagesAdapter` — абстрактные слои для Callback API и VK Messages. Готовый вариант объединяет их: `VKMessagesCallbackAdapter`. Адаптер создаётся вручную и передаётся готовым инстансом в `app.add_adapter(adapter)`; `App.start()` запускает зарегистрированные адаптеры, а новые адаптеры, добавленные во время работы, запускаются асинхронно в текущем event loop. `App.close()` останавливает запущенные адаптеры в обратном порядке. Если `messages.edit` завершился ошибкой, `redraw()` логирует её и отправляет новое сообщение вместо падения webhook'а.
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
adapter = app.add_adapter(VKMessagesCallbackAdapter(app, vk, group_id=group_id))
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Вложения принимают типы VKX или VK-строки, кнопки — модели `vkx.KeyboardButton`. `VKInterface.callback(..., color=...)` принимает `KeyboardButtonColor` или строку и проверяет допустимые значения. `KeyboardButtonColor` реэкспортируется из `vkuxt` и `vkuxt.adapters.vk`, поэтому отдельный импорт из `vkx.models` не нужен.
|
|
176
|
+
|
|
177
|
+
`await ctx.redraw(view)` выбирает интерфейс текущего события: редактирует сообщение callback-кнопки, отправляет ответ на текст и удаляет старый интерфейс либо отправляет новое сообщение. Для чужого или явно выбранного интерфейса используйте `await adapter.redraw(view, target=handle)`; `peer_id=...` выбирает последний интерфейс диалога. `await ctx.redraw(None)` удаляет текущий интерфейс. `await adapter.send(peer_id, view)` и `await adapter.edit(handle, view)` доступны для явных операций.
|
|
178
|
+
|
|
179
|
+
`App.lifespan` можно передать напрямую в Starlette/FastAPI, чтобы запускать и останавливать адаптеры вместе с ASGI-приложением:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
web = FastAPI(lifespan=app.lifespan)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
VK Callback адаптер использует `vkx.server.CallbackServer` для проверки входа и разбора уведомлений в модели `vkx`. Дедупликация доставленных событий — ответственность `vkx`: `CallbackServer` отбрасывает повторный `event_id` и повторную доставку `message_new` с той же парой `peer_id` + `conversation_message_id`. У `message_event` идентификатор лежит внутри `object.event_id`, поэтому адаптер дополнительно передаёт его в `CallbackServer.is_duplicate`. Текст приходит VK-хендлеру аргументом `text`; адаптер сам определяет, было ли сообщение ответом на сохранённый интерфейс. В личных диалогах можно включить определение последнего активного интерфейса. Если foreground-хендлер не привёл к edit/delete именно нажатого интерфейса, адаптер отправляет `messages.sendMessageEventAnswer`. Индекс последних интерфейсов peer'а ограничен 10 записями и получает TTL интерфейса.
|
|
186
|
+
|
|
187
|
+
При fire-and-forget обработке `sendMessageEventAnswer` отправляется сразу после запуска фоновой задачи, не дожидаясь её завершения. Это укладывается в окно ответа VK; если нужен результат хендлера до ответа, используйте `background=False`, но держите такой путь коротким.
|
|
188
|
+
|
|
189
|
+
Полный скетч крестиков-ноликов, включая лобби с кодом, ввод кода ответом на сообщение, две синхронизируемые доски и сдачу, находится в [`examples/tictactoe.py`](examples/tictactoe.py).
|
|
190
|
+
|
|
File without changes
|