python-vk-bot 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,154 @@
1
+ """Reusable HTTP transport; request credentials never become client defaults."""
2
+
3
+ import logging
4
+ import re
5
+ from collections.abc import Mapping
6
+ from types import TracebackType
7
+ from typing import Self, cast
8
+
9
+ import httpx
10
+ from pydantic import JsonValue, SecretStr, ValidationError
11
+
12
+ from ._config import TransportConfig
13
+ from .errors import (
14
+ APIError,
15
+ HTTPError,
16
+ InvalidResponseError,
17
+ LifecycleError,
18
+ NetworkError,
19
+ RateLimitError,
20
+ )
21
+ from .models.api import APIEnvelope
22
+
23
+ type Parameter = str | int | float | bool | None
24
+
25
+
26
+ class _RedactQuery(logging.Filter):
27
+ """HTTPX INFO logs otherwise expose the Long Poll key in request URLs."""
28
+
29
+ def filter(self, record: logging.LogRecord) -> bool:
30
+ if isinstance(record.args, tuple):
31
+ record.args = tuple(
32
+ value.copy_with(query=None)
33
+ if isinstance(value, httpx.URL) and value.query
34
+ else value
35
+ for value in record.args
36
+ )
37
+ return True
38
+
39
+
40
+ class HTTPTransport:
41
+ """Own a client or borrow an injected client without closing it."""
42
+
43
+ def __init__(self, config: TransportConfig, *, client: httpx.AsyncClient | None = None) -> None:
44
+ self.config = config
45
+ self._client = client
46
+ self._owns_client = client is None
47
+ self._started = False
48
+ httpx_logger = logging.getLogger("httpx")
49
+ if not any(isinstance(item, _RedactQuery) for item in httpx_logger.filters):
50
+ httpx_logger.addFilter(_RedactQuery())
51
+
52
+ async def initialize(self) -> None:
53
+ if self._started:
54
+ return
55
+ if self._client is None:
56
+ self._client = httpx.AsyncClient(timeout=self.config.timeout)
57
+ if self._client.is_closed:
58
+ raise LifecycleError("The injected HTTP client is closed")
59
+ if "authorization" in self._client.headers:
60
+ raise ValueError("Injected clients must not define a default Authorization header")
61
+ if {"access_token", "key", "secret"}.intersection(self._client.params):
62
+ raise ValueError("Injected clients must not define credential query parameters")
63
+ self._started = True
64
+
65
+ async def shutdown(self) -> None:
66
+ self._started = False
67
+ if self._owns_client and self._client is not None:
68
+ await self._client.aclose()
69
+ self._client = None
70
+
71
+ async def __aenter__(self) -> Self:
72
+ await self.initialize()
73
+ return self
74
+
75
+ async def __aexit__(
76
+ self,
77
+ exc_type: type[BaseException] | None,
78
+ exc: BaseException | None,
79
+ traceback: TracebackType | None,
80
+ ) -> None:
81
+ await self.shutdown()
82
+
83
+ async def _request(
84
+ self,
85
+ method: str,
86
+ url: str,
87
+ *,
88
+ data: Mapping[str, str] | None = None,
89
+ params: Mapping[str, str] | None = None,
90
+ headers: Mapping[str, str] | None = None,
91
+ timeout: float | None = None,
92
+ ) -> JsonValue:
93
+ if not self._started or self._client is None:
94
+ raise LifecycleError("Initialize the bot or use its async context manager")
95
+ try:
96
+ response = await self._client.request(
97
+ method,
98
+ url,
99
+ data=data,
100
+ params=params,
101
+ headers=headers,
102
+ timeout=self.config.timeout if timeout is None else timeout,
103
+ follow_redirects=False,
104
+ auth=None,
105
+ )
106
+ except httpx.RequestError:
107
+ raise NetworkError("VK HTTP request could not complete") from None
108
+ if not response.is_success:
109
+ raise HTTPError(response.status_code)
110
+ try:
111
+ return cast(JsonValue, response.json())
112
+ except ValueError:
113
+ raise InvalidResponseError("VK returned invalid JSON") from None
114
+
115
+ async def call(self, method: str, parameters: Mapping[str, Parameter]) -> JsonValue:
116
+ if re.fullmatch(r"[A-Za-z]+\.[A-Za-z]+", method) is None:
117
+ raise ValueError("Expected a VK method name such as messages.send")
118
+ if {"v", "access_token"}.intersection(parameters):
119
+ raise ValueError("Version and credentials belong to the transport configuration")
120
+ data = {
121
+ key: str(int(value)) if isinstance(value, bool) else str(value)
122
+ for key, value in parameters.items()
123
+ if value is not None
124
+ }
125
+ data["v"] = self.config.api_version
126
+ payload = await self._request(
127
+ "POST",
128
+ f"https://api.vk.ru/method/{method}",
129
+ data=data,
130
+ headers={"Authorization": f"Bearer {self.config.token.get_secret_value()}"},
131
+ )
132
+ try:
133
+ envelope = APIEnvelope.model_validate(payload)
134
+ except ValidationError:
135
+ raise InvalidResponseError("Invalid VK API envelope") from None
136
+ if envelope.error is not None:
137
+ code = envelope.error.error_code
138
+ error_type = RateLimitError if code == 6 else APIError
139
+ raise error_type(code, method)
140
+ if "response" not in envelope.model_fields_set:
141
+ raise InvalidResponseError("VK API envelope has no response")
142
+ return envelope.response
143
+
144
+ async def poll(self, server: str, *, key: SecretStr, ts: str, wait: int) -> JsonValue:
145
+ url = httpx.URL(server)
146
+ if url.scheme != "https" or not url.host or url.userinfo or url.query or url.fragment:
147
+ raise InvalidResponseError("Unexpected Long Poll server URL")
148
+ # The community token is only sent to api.vk.ru, never to the polling server.
149
+ return await self._request(
150
+ "GET",
151
+ server,
152
+ params={"act": "a_check", "key": key.get_secret_value(), "ts": ts, "wait": str(wait)},
153
+ timeout=max(self.config.timeout, wait + 10.0),
154
+ )
@@ -0,0 +1,237 @@
1
+ Metadata-Version: 2.5
2
+ Name: python-vk-bot
3
+ Version: 0.1.0
4
+ Summary: An asynchronous framework for VK community bots.
5
+ Author-email: Aleksei Fomenko <fomenko_ai@proton.me>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: asyncio,bot,vk
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3.14
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.14
14
+ Requires-Dist: httpx<1,>=0.28
15
+ Requires-Dist: pydantic-settings<3,>=2.12
16
+ Requires-Dist: pydantic<3,>=2.12
17
+ Provides-Extra: callback
18
+ Requires-Dist: starlette<1,>=0.49; extra == 'callback'
19
+ Requires-Dist: uvicorn<1,>=0.38; extra == 'callback'
20
+ Description-Content-Type: text/markdown
21
+
22
+ <p align="center">
23
+ <img src="logo.png" alt="python-vk-bot logo" width="720">
24
+ </p>
25
+
26
+ # python-vk-bot
27
+
28
+ An asynchronous framework for VK community bots, inspired by python-telegram-bot.
29
+ Development targets **Python 3.14** and VK API **5.199**. The regression suite runs without
30
+ network access; see [verification results](docs/development/verification.md).
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ uv add python-vk-bot
36
+ ```
37
+
38
+ ### Install from this checkout
39
+
40
+ ```bash
41
+ uv sync --locked
42
+ ```
43
+
44
+ For the optional Callback HTTP receiver:
45
+
46
+ ```bash
47
+ uv sync --locked --extra callback
48
+ ```
49
+
50
+ ## Long Poll bot
51
+
52
+ Enable Bots Long Poll and the `message_new` event in the community settings. Configure
53
+ its event version to match the supported API schema. Supply a community token with the
54
+ required permissions through the environment; messaging is subject to VK's conversation
55
+ and permission rules.
56
+
57
+ ```python
58
+ import asyncio
59
+
60
+ from python_vk_bot import Application, Bot, BotSettings, Context, MessageHandler, Update, filters
61
+
62
+
63
+ async def echo(update: Update, context: Context) -> None:
64
+ message = update.message
65
+ if message is not None:
66
+ await context.bot.send_message(message.peer_id, message.text)
67
+
68
+
69
+ async def main() -> None:
70
+ settings = BotSettings()
71
+ bot = Bot(settings.token, api_version=settings.api_version, timeout=settings.timeout)
72
+ app = Application(bot, settings.group_id)
73
+ app.add_handler(MessageHandler(echo, predicate=filters.TEXT))
74
+ await app.run_polling(wait=settings.polling_wait)
75
+
76
+
77
+ asyncio.run(main())
78
+ ```
79
+
80
+ With `VK_TOKEN` and `VK_GROUP_ID` already set, run the included example:
81
+
82
+ ```bash
83
+ uv run python examples/echo.py
84
+ ```
85
+
86
+ `run_polling()` is awaitable. Cancellation closes owned resources. Handlers run
87
+ sequentially, with the first match in each numeric group. `Context` exposes `bot`,
88
+ `user_data`, `peer_data` and `bot_data`. Use `MessageEventHandler` for button events,
89
+ `filters.LOCATION` for messages containing geo, and `CommandHandler` for `/command` tokens.
90
+
91
+ ## Settings
92
+
93
+ Import `BotSettings` from `python_vk_bot` or `python_vk_bot.settings`. It uses
94
+ `pydantic-settings` to load and validate configuration when you create an instance.
95
+ Importing the module does not read configuration or require credentials.
96
+
97
+ | Field | Environment variable | Default |
98
+ | --- | --- | --- |
99
+ | `token` | `VK_TOKEN` | Required, nonempty `SecretStr` |
100
+ | `group_id` | `VK_GROUP_ID` | Required, positive integer |
101
+ | `api_version` | `VK_API_VERSION` | `"5.199"` |
102
+ | `timeout` | `VK_TIMEOUT` | `40.0` seconds, positive and finite |
103
+ | `polling_wait` | `VK_POLLING_WAIT` | `25` seconds, between 1 and 90 |
104
+
105
+ Use environment variables directly:
106
+
107
+ ```python
108
+ from python_vk_bot import BotSettings
109
+
110
+ settings = BotSettings() # Read VK_* variables; no automatic .env loading.
111
+ ```
112
+
113
+ Alternatively, opt into a dotenv file and override a value for this instance:
114
+
115
+ ```python
116
+ from pathlib import Path
117
+
118
+ from python_vk_bot import BotSettings
119
+
120
+ settings = BotSettings(_env_file=Path(".env"), timeout=15.0)
121
+ ```
122
+
123
+ Explicit constructor values take precedence over environment variables, which take
124
+ precedence over dotenv values and field defaults. Unrelated dotenv entries are ignored,
125
+ so a project may share its `.env` with other components. Unknown constructor fields are
126
+ also ignored; use `extra="forbid"` in a subclass if you prefer strict configuration.
127
+ Settings only contain configuration: pass their values to `Bot`, `Application` and
128
+ `run_polling` explicitly as shown above. Keep real tokens and `.env` out of Git.
129
+
130
+ Extend the class with your project's fields:
131
+
132
+ ```python
133
+ from pathlib import Path
134
+
135
+ from pydantic import Field
136
+
137
+ from python_vk_bot import BotSettings
138
+
139
+
140
+ class ProjectSettings(BotSettings):
141
+ admin_ids: list[int] = Field(default_factory=list)
142
+ debug: bool = False
143
+
144
+
145
+ settings = ProjectSettings(_env_file=Path(".env"))
146
+ ```
147
+
148
+ These fields read `VK_ADMIN_IDS` (a JSON array such as `[123, 456]`) and `VK_DEBUG`
149
+ (for example, `true`). Override `model_config` with `SettingsConfigDict(env_prefix="MYBOT_")`
150
+ to use another prefix. A field with `Field(validation_alias="DATABASE_URL")` instead reads
151
+ that exact variable name, without the prefix. Token values remain `SecretStr` and can be
152
+ passed directly to `Bot`.
153
+
154
+ ## Bot without polling
155
+
156
+ ```python
157
+ async with Bot(token) as bot:
158
+ user = await bot.get_user_info(user_id)
159
+ ```
160
+
161
+ An injected `httpx.AsyncClient` remains caller-owned. Methods cover sending, editing,
162
+ forwarding, fetching a message/user, answering message events, and obtaining Long Poll
163
+ connection data. `Bot.request(method, parameters)` exposes raw JSON results for additional
164
+ VK methods; nested request parameters must be JSON encoded explicitly.
165
+
166
+ Messages use `peer_id`; `Message` exposes both `id` and `conversation_message_id`.
167
+ `send_message` returns an integer message ID and accepts an optional nonzero int32
168
+ `random_id`. Sends are never retried automatically. `edit_message` accepts exactly one
169
+ message identifier and maps `conversation_message_id` to VK's `cmid` parameter.
170
+
171
+ ## Callback API
172
+
173
+ The [Callback example](examples/callback.py) extends the same base settings with its
174
+ required secrets. For example, to construct a receiver from the environment:
175
+
176
+ ```python
177
+ from pydantic import Field, SecretStr
178
+
179
+ from python_vk_bot import Application, Bot, BotSettings, Callback
180
+
181
+
182
+ class CallbackSettings(BotSettings):
183
+ callback_secret: SecretStr = Field(min_length=1, repr=False)
184
+ callback_confirmation: SecretStr = Field(min_length=1, repr=False)
185
+
186
+
187
+ settings = CallbackSettings()
188
+ bot = Bot(settings.token, api_version=settings.api_version, timeout=settings.timeout)
189
+ application = Application(bot, settings.group_id)
190
+ # Register your handlers before starting the ASGI application.
191
+ callback = Callback(
192
+ application,
193
+ secret=settings.callback_secret,
194
+ confirmation_code=settings.callback_confirmation,
195
+ )
196
+ asgi_app = callback.create_app()
197
+ ```
198
+
199
+ Set `VK_TOKEN`, `VK_GROUP_ID`, `VK_CALLBACK_SECRET`, and `VK_CALLBACK_CONFIRMATION`, then:
200
+
201
+ ```bash
202
+ uv run --extra callback uvicorn examples.callback:create_app --factory
203
+ ```
204
+
205
+ Expose `/callback` through your HTTPS deployment and configure the same secret in VK.
206
+ ASGI lifespan manages startup/shutdown. `Callback.handle()` is also available for other
207
+ HTTP frameworks, provided the caller manages Application's async lifecycle.
208
+
209
+ Confirmation returns the configured confirmation code. Ordinary events return `ok` only
210
+ after successful processing. A bounded in-memory cache skips successful duplicates.
211
+ Partial handler failures, restarts and multiple workers can repeat side effects; this
212
+ is not a durable queue or an exactly-once processing guarantee. See the architecture
213
+ for delivery limitations and resource limits.
214
+
215
+ ## Agent Skills
216
+
217
+ The bundled **telegram-to-vk** Agent Skill helps AI coding agents migrate
218
+ `python-telegram-bot` applications to VK using `python-vk-bot`.
219
+
220
+ After installing a package version containing the skill, run from your bot project's root:
221
+
222
+ ```bash
223
+ uvx library-skills
224
+ ```
225
+
226
+ Select `telegram-to-vk` and the `.agents/skills` target for a compatible AI coding agent.
227
+ The skill adds no runtime dependencies and does not change the framework's behavior.
228
+ See [Agent Skills](docs/agent-skills.md) for package installation, installing from a local
229
+ checkout, and usage examples.
230
+
231
+ ## Documentation
232
+
233
+ - [Architecture and lifecycle](docs/architecture/project-architecture.md)
234
+ - [python-telegram-bot design mapping](docs/architecture/python-telegram-bot-compatibility.md)
235
+ - [VK API support](docs/development/api-compatibility.md)
236
+ - [Contributing and verification](CONTRIBUTING.md)
237
+ - [Changelog](CHANGELOG.md)
@@ -0,0 +1,24 @@
1
+ python_vk_bot/__init__.py,sha256=4Qy_CyBUsznCQJ5h3I2VVLMQNWUoTlQc5mOCjYVUMIM,693
2
+ python_vk_bot/_config.py,sha256=Rju-sjCKAfk70ajQbpPMzOuefBIjtgF8tKjOuSYo0AE,744
3
+ python_vk_bot/application.py,sha256=rHjKVJRUTGwqgpp7DRJrZOV1eP-_LZ9_ei90EYsTQcs,8741
4
+ python_vk_bot/bot.py,sha256=rZgunz4lG-P2EFxLMuTl3Smory8soeIbe00YOVkyJ78,6386
5
+ python_vk_bot/callback.py,sha256=9j11jtl4rpTnfUThvOf2g6kb7wYQXAQerHbuk3IYw3o,4726
6
+ python_vk_bot/context.py,sha256=pzsnUSdJsVLCAsCeItf0L804HclyDtzG2laZrLlHDpk,445
7
+ python_vk_bot/errors.py,sha256=36sWLepB5JoZh2-n89wA2CiaEzbKES86Qc818dLkfCw,1148
8
+ python_vk_bot/filters.py,sha256=5fpOdxTZvFmrgokWUvYj1nowCWvPlLt9EUhRmB-0HVg,1285
9
+ python_vk_bot/handlers.py,sha256=iG91bzA40evZFY6Qp78nvoz_O4pjG0lE6p1IwLuCQSY,1974
10
+ python_vk_bot/long_poll.py,sha256=rmAxflq0E3H-tMDybQ7fBf1cKsJQFnaU4VbyQS3BSw4,3777
11
+ python_vk_bot/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ python_vk_bot/settings.py,sha256=JcbDihGRJGRGvBBSeB3j_LPaoX7dkPHfh-WviKQCrwk,915
13
+ python_vk_bot/transport.py,sha256=sfNIEtiKGvaUgU2wm4IrWjrIU1CXMwtR9TnaoBXUzfM,5833
14
+ python_vk_bot/.agents/skills/telegram-to-vk/SKILL.md,sha256=WMrNoxLkhhlu8wnS9rFpZi3OlwBlVNHCYFSNtKmkJzM,2828
15
+ python_vk_bot/.agents/skills/telegram-to-vk/references/migration-mapping.md,sha256=KFxmCQyKbUK7Z0JuS39FocyoFdtnslLLp9j2pNeJAds,9019
16
+ python_vk_bot/models/__init__.py,sha256=L9huCyAM2WWt5JKXyaC8dS0OfTydubdtrGUmvvTnyak,196
17
+ python_vk_bot/models/api.py,sha256=WrPGsBOIO1yFD376b0nlVY75rZWhJPXEJ4VzZeUW6uI,931
18
+ python_vk_bot/models/base.py,sha256=LYcbevtU8jq5QVLo8b1mOv70RzgEYjPtTmhMMe-JN3A,249
19
+ python_vk_bot/models/events.py,sha256=bS8XC0TJrj_AG0u6H9fs35kMkg47EeVoxtaUH7ya1gM,2222
20
+ python_vk_bot/models/messages.py,sha256=CmchlimGmKfj_i-ogHPDZbAk0vPwHKUymfMr_bsWlPc,1223
21
+ python_vk_bot-0.1.0.dist-info/METADATA,sha256=61pB87s_jpj9l0CI6CvvMe-r_av1dGnwonXWexwQJ9Y,8275
22
+ python_vk_bot-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
23
+ python_vk_bot-0.1.0.dist-info/licenses/LICENSE,sha256=F-35ycqPcYW243OjxMHCggr_AHQgZLX2zvtZgobVEvo,1072
24
+ python_vk_bot-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aleksei Fomenko
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.