vkx 0.2.2__tar.gz → 0.2.4__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 (134) hide show
  1. {vkx-0.2.2 → vkx-0.2.4}/PKG-INFO +59 -5
  2. {vkx-0.2.2 → vkx-0.2.4}/README.md +58 -4
  3. {vkx-0.2.2 → vkx-0.2.4}/pyproject.toml +1 -1
  4. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/__init__.py +2 -0
  5. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/client/__init__.py +2 -0
  6. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/client/client.py +70 -13
  7. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/client/http.py +15 -0
  8. vkx-0.2.4/src/vkx/client/storage.py +458 -0
  9. vkx-0.2.4/src/vkx/client/tokens.py +813 -0
  10. vkx-0.2.2/src/vkx/client/storage.py +0 -255
  11. vkx-0.2.2/src/vkx/client/tokens.py +0 -427
  12. {vkx-0.2.2 → vkx-0.2.4}/.gitignore +0 -0
  13. {vkx-0.2.2 → vkx-0.2.4}/LICENSE +0 -0
  14. {vkx-0.2.2 → vkx-0.2.4}/examples/README.md +0 -0
  15. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/adapters/__init__.py +0 -0
  16. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/adapters/_common.py +0 -0
  17. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/adapters/aiohttp.py +0 -0
  18. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/adapters/django.py +0 -0
  19. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/adapters/fastapi.py +0 -0
  20. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/adapters/flask.py +0 -0
  21. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/adapters/sanic.py +0 -0
  22. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/adapters/starlette.py +0 -0
  23. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/client/errors.py +0 -0
  24. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/client/pagination.py +0 -0
  25. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/client/resolve.py +0 -0
  26. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/client/upload.py +0 -0
  27. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/__init__.py +0 -0
  28. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/base_model.py +0 -0
  29. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/categories.py +0 -0
  30. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/events/__init__.py +0 -0
  31. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/events/bot_events.py +0 -0
  32. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/events/callback_events.py +0 -0
  33. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/events/enums/__init__.py +0 -0
  34. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/events/enums/callback_events.py +0 -0
  35. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/events/enums/group_events.py +0 -0
  36. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/events/enums/user_events.py +0 -0
  37. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/events/objects/__init__.py +0 -0
  38. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/events/objects/group_event_objects.py +0 -0
  39. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/events/objects/user_event_objects.py +0 -0
  40. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/events/user_events.py +0 -0
  41. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/formatting.py +0 -0
  42. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/__init__.py +0 -0
  43. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/account.py +0 -0
  44. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/ads.py +0 -0
  45. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/app_widgets.py +0 -0
  46. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/apps.py +0 -0
  47. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/auth.py +0 -0
  48. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/base_category.py +0 -0
  49. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/board.py +0 -0
  50. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/bugtracker.py +0 -0
  51. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/calls.py +0 -0
  52. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/database.py +0 -0
  53. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/docs.py +0 -0
  54. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/donut.py +0 -0
  55. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/downloaded_games.py +0 -0
  56. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/fave.py +0 -0
  57. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/friends.py +0 -0
  58. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/gifts.py +0 -0
  59. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/groups.py +0 -0
  60. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/lead_forms.py +0 -0
  61. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/likes.py +0 -0
  62. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/market.py +0 -0
  63. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/messages.py +0 -0
  64. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/newsfeed.py +0 -0
  65. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/notes.py +0 -0
  66. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/notifications.py +0 -0
  67. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/orders.py +0 -0
  68. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/pages.py +0 -0
  69. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/photos.py +0 -0
  70. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/podcasts.py +0 -0
  71. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/polls.py +0 -0
  72. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/pretty_cards.py +0 -0
  73. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/search.py +0 -0
  74. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/secure.py +0 -0
  75. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/stats.py +0 -0
  76. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/status.py +0 -0
  77. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/storage.py +0 -0
  78. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/store.py +0 -0
  79. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/stories.py +0 -0
  80. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/streaming.py +0 -0
  81. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/translations.py +0 -0
  82. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/users.py +0 -0
  83. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/utils.py +0 -0
  84. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/video.py +0 -0
  85. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/wall.py +0 -0
  86. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/methods/widgets.py +0 -0
  87. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/objects.py +0 -0
  88. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/__init__.py +0 -0
  89. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/account.py +0 -0
  90. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/ads.py +0 -0
  91. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/app_widgets.py +0 -0
  92. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/apps.py +0 -0
  93. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/auth.py +0 -0
  94. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/base.py +0 -0
  95. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/board.py +0 -0
  96. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/bugtracker.py +0 -0
  97. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/calls.py +0 -0
  98. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/database.py +0 -0
  99. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/docs.py +0 -0
  100. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/donut.py +0 -0
  101. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/downloaded_games.py +0 -0
  102. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/fave.py +0 -0
  103. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/friends.py +0 -0
  104. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/gifts.py +0 -0
  105. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/groups.py +0 -0
  106. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/lead_forms.py +0 -0
  107. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/likes.py +0 -0
  108. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/market.py +0 -0
  109. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/messages.py +0 -0
  110. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/newsfeed.py +0 -0
  111. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/notes.py +0 -0
  112. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/notifications.py +0 -0
  113. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/orders.py +0 -0
  114. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/pages.py +0 -0
  115. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/photos.py +0 -0
  116. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/podcasts.py +0 -0
  117. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/pretty_cards.py +0 -0
  118. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/search.py +0 -0
  119. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/store.py +0 -0
  120. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/stories.py +0 -0
  121. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/streaming.py +0 -0
  122. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/translations.py +0 -0
  123. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/users.py +0 -0
  124. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/utils.py +0 -0
  125. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/video.py +0 -0
  126. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/wall.py +0 -0
  127. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/models/responses/widgets.py +0 -0
  128. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/py.typed +0 -0
  129. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/server/__init__.py +0 -0
  130. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/server/_polling.py +0 -0
  131. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/server/bots_longpoll.py +0 -0
  132. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/server/callback.py +0 -0
  133. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/server/callback_server.py +0 -0
  134. {vkx-0.2.2 → vkx-0.2.4}/src/vkx/server/longpoll.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vkx
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Асинхронный клиент VK API: выбор токена по scope, батчинг через execute, типы API/событий и разбор Callback/Long Poll.
5
5
  Project-URL: Homepage, https://github.com/matrixd0t/vkx
6
6
  Project-URL: Repository, https://github.com/matrixd0t/vkx
@@ -100,18 +100,24 @@ asyncio.run(main())
100
100
 
101
101
  ### Создание
102
102
 
103
- Передать можно статичный токен, веб-cookies, готовые источники или всё вместе:
103
+ Передать можно статичный токен, веб-cookies, логин/пароль, готовые источники или всё вместе:
104
104
 
105
105
  ```python
106
- from vkx import VKClient
106
+ from vkx import VKClient, open_store
107
107
 
108
108
  vk = VKClient(tokens="vk1.a....")
109
109
  vk = VKClient(cookies={"p": "...", "remixsid": "..."})
110
110
  vk = VKClient(tokens=["tok_a", "tok_b"], cookies=[{"p": ..., "remixsid": ...}])
111
+ vk = VKClient(cookies="cookies.json") # в том числе JSON-файл, созданный vkqr
112
+ vk = VKClient(login="user@example.com", password="...", store=open_store("auth.json"))
113
+ # переопределить User-Agent HTTP-клиента:
114
+ vk = VKClient(tokens="vk1.a....", user_agent="MyApp/1.0")
111
115
  ```
112
116
 
113
117
  Конструктор не ходит в сеть; сеть трогается при первом `call()` или явном
114
118
  `await vk.probe()`.
119
+ Если передан `user_agent`, он заменяет заголовок `User-Agent` HTTP-клиента;
120
+ переданный вручную `http_client` должен предоставлять изменяемый `headers`.
115
121
 
116
122
  ### Владелец и права
117
123
 
@@ -144,10 +150,58 @@ countries = await vk.database.get_countries(count=1500)
144
150
 
145
151
  ```python
146
152
  vk = VKClient(cookies={"p": "...", "remixsid": "..."})
153
+ vk = VKClient(cookies="cookies.json")
154
+ ```
155
+
156
+ Принимается словарь cookie, список таких словарей или JSON-файл. Поддерживаются
157
+ простой JSON-объект `{"p": "...", "remixsid": "..."}`, список объектов и формат
158
+ [`vkqr`](https://github.com/matrixd0t/vkqr):
159
+
160
+ ```json
161
+ {
162
+ "cookies": [
163
+ {
164
+ "created_at": 1700000000,
165
+ "user_id": 580106999,
166
+ "p": "...",
167
+ "remixsid": "..."
168
+ }
169
+ ]
170
+ }
171
+ ```
172
+
173
+ Для формата `vkqr` можно передать путь прямо в `VKClient` — файл сам читается,
174
+ метаданные `created_at` и `user_id` не отправляются как cookies. Веб-токен
175
+ получается и обновляется автоматически; при переданном `store` токен и владелец
176
+ сохраняются в нём. `open_store()` без пути создаёт SQLite в `.vkx/vkx.sqlite`.
177
+ Если `open_store("cookies.json")`
178
+ используется с JSON-файлом `vkqr`, VKX сохраняет cookie-записи в том же массиве
179
+ `cookies`, а кеш токенов и состояния обычных токенов — в служебном поле `_vkx`.
180
+ Старый формат JSONStore читается и преобразуется при следующем сохранении. Если
181
+ в файле несколько разных аккаунтов, один `VKClient` всё равно будет привязан к
182
+ первому успешно проверенному владельцу; для отдельных аккаунтов создавайте
183
+ отдельные клиенты.
184
+
185
+ ### Авторизация по логину и паролю
186
+
187
+ ```python
188
+ from vkx import VKClient, open_store
189
+
190
+ store = open_store("auth.json")
191
+ vk = VKClient(login="user@example.com", password="...", store=store)
147
192
  ```
148
193
 
149
- Веб-токен получается и обновляется автоматически; токен и владелец сохраняются
150
- в Store (по умолчанию SQLite в `.vkx/vkx.sqlite`).
194
+ Источник сначала пробует сохранённые cookies и обновляет по ним web-токен. Если
195
+ VK отклоняет cookie-сессию, он пробует получить новые cookies через VK ID по
196
+ логину/паролю, сохраняет их и повторяет получение web-токена. При следующем
197
+ запуске можно указать только `login` и тот же `store`: сохранённый пароль будет
198
+ восстановлен. Если для входа VK требует капчу или код подтверждения, авторизация
199
+ только по логину/паролю завершится понятной ошибкой — автоматического обхода
200
+ дополнительной проверки нет.
201
+
202
+ Пароль сохраняется в Store **без шифрования**. Ограничьте доступ к файлу/БД и не
203
+ коммитьте их в репозиторий; для JSON используйте права доступа, ограниченные
204
+ текущим пользователем.
151
205
 
152
206
  ### `resolve()`
153
207
 
@@ -72,18 +72,24 @@ asyncio.run(main())
72
72
 
73
73
  ### Создание
74
74
 
75
- Передать можно статичный токен, веб-cookies, готовые источники или всё вместе:
75
+ Передать можно статичный токен, веб-cookies, логин/пароль, готовые источники или всё вместе:
76
76
 
77
77
  ```python
78
- from vkx import VKClient
78
+ from vkx import VKClient, open_store
79
79
 
80
80
  vk = VKClient(tokens="vk1.a....")
81
81
  vk = VKClient(cookies={"p": "...", "remixsid": "..."})
82
82
  vk = VKClient(tokens=["tok_a", "tok_b"], cookies=[{"p": ..., "remixsid": ...}])
83
+ vk = VKClient(cookies="cookies.json") # в том числе JSON-файл, созданный vkqr
84
+ vk = VKClient(login="user@example.com", password="...", store=open_store("auth.json"))
85
+ # переопределить User-Agent HTTP-клиента:
86
+ vk = VKClient(tokens="vk1.a....", user_agent="MyApp/1.0")
83
87
  ```
84
88
 
85
89
  Конструктор не ходит в сеть; сеть трогается при первом `call()` или явном
86
90
  `await vk.probe()`.
91
+ Если передан `user_agent`, он заменяет заголовок `User-Agent` HTTP-клиента;
92
+ переданный вручную `http_client` должен предоставлять изменяемый `headers`.
87
93
 
88
94
  ### Владелец и права
89
95
 
@@ -116,10 +122,58 @@ countries = await vk.database.get_countries(count=1500)
116
122
 
117
123
  ```python
118
124
  vk = VKClient(cookies={"p": "...", "remixsid": "..."})
125
+ vk = VKClient(cookies="cookies.json")
126
+ ```
127
+
128
+ Принимается словарь cookie, список таких словарей или JSON-файл. Поддерживаются
129
+ простой JSON-объект `{"p": "...", "remixsid": "..."}`, список объектов и формат
130
+ [`vkqr`](https://github.com/matrixd0t/vkqr):
131
+
132
+ ```json
133
+ {
134
+ "cookies": [
135
+ {
136
+ "created_at": 1700000000,
137
+ "user_id": 580106999,
138
+ "p": "...",
139
+ "remixsid": "..."
140
+ }
141
+ ]
142
+ }
143
+ ```
144
+
145
+ Для формата `vkqr` можно передать путь прямо в `VKClient` — файл сам читается,
146
+ метаданные `created_at` и `user_id` не отправляются как cookies. Веб-токен
147
+ получается и обновляется автоматически; при переданном `store` токен и владелец
148
+ сохраняются в нём. `open_store()` без пути создаёт SQLite в `.vkx/vkx.sqlite`.
149
+ Если `open_store("cookies.json")`
150
+ используется с JSON-файлом `vkqr`, VKX сохраняет cookie-записи в том же массиве
151
+ `cookies`, а кеш токенов и состояния обычных токенов — в служебном поле `_vkx`.
152
+ Старый формат JSONStore читается и преобразуется при следующем сохранении. Если
153
+ в файле несколько разных аккаунтов, один `VKClient` всё равно будет привязан к
154
+ первому успешно проверенному владельцу; для отдельных аккаунтов создавайте
155
+ отдельные клиенты.
156
+
157
+ ### Авторизация по логину и паролю
158
+
159
+ ```python
160
+ from vkx import VKClient, open_store
161
+
162
+ store = open_store("auth.json")
163
+ vk = VKClient(login="user@example.com", password="...", store=store)
119
164
  ```
120
165
 
121
- Веб-токен получается и обновляется автоматически; токен и владелец сохраняются
122
- в Store (по умолчанию SQLite в `.vkx/vkx.sqlite`).
166
+ Источник сначала пробует сохранённые cookies и обновляет по ним web-токен. Если
167
+ VK отклоняет cookie-сессию, он пробует получить новые cookies через VK ID по
168
+ логину/паролю, сохраняет их и повторяет получение web-токена. При следующем
169
+ запуске можно указать только `login` и тот же `store`: сохранённый пароль будет
170
+ восстановлен. Если для входа VK требует капчу или код подтверждения, авторизация
171
+ только по логину/паролю завершится понятной ошибкой — автоматического обхода
172
+ дополнительной проверки нет.
173
+
174
+ Пароль сохраняется в Store **без шифрования**. Ограничьте доступ к файлу/БД и не
175
+ коммитьте их в репозиторий; для JSON используйте права доступа, ограниченные
176
+ текущим пользователем.
123
177
 
124
178
  ### `resolve()`
125
179
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "vkx"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  description = "Асинхронный клиент VK API: выбор токена по scope, батчинг через execute, типы API/событий и разбор Callback/Long Poll."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -25,6 +25,7 @@ from .client import (
25
25
  HttpResponse,
26
26
  JSONStore,
27
27
  MemoryStore,
28
+ PasswordCookieSource,
28
29
  PendingApiCall,
29
30
  ResolveCache,
30
31
  ResolvedMention,
@@ -102,6 +103,7 @@ __all__ = [
102
103
  "JSONStore",
103
104
  "LongPoll",
104
105
  "MemoryStore",
106
+ "PasswordCookieSource",
105
107
  "PendingApiCall",
106
108
  "ResolveCache",
107
109
  "ResolveResult",
@@ -59,6 +59,7 @@ from .storage import (
59
59
  from .tokens import (
60
60
  CATEGORY_SCOPES,
61
61
  SCOPE_TITLES,
62
+ PasswordCookieSource,
62
63
  StaticTokenSource,
63
64
  TokenSource,
64
65
  TokenSourceError,
@@ -94,6 +95,7 @@ __all__ = [
94
95
  "HttpResponse",
95
96
  "JSONStore",
96
97
  "MemoryStore",
98
+ "PasswordCookieSource",
97
99
  "PendingApiCall",
98
100
  "ResolveCache",
99
101
  "ResolveResult",
@@ -10,8 +10,9 @@ groups.getById (токен группы). Владельцем становит
10
10
  vk.permissions # ['Сообщения', 'Друзья', ...]
11
11
  await vk.call("messages.send", peer_id=1, message="hi")
12
12
 
13
- Источники токенов задаются тремя необязательными полями: tokens (строки или
14
- TokenSource), cookies (словари с p и remixsid) и sources (готовые TokenSource).
13
+ Источники токенов задаются необязательными tokens (строки или TokenSource),
14
+ cookies (словари, JSON-записи или путь к JSON-файлу), login/password или sources
15
+ (готовые TokenSource).
15
16
  Из tokens/cookies источники создаются автоматически; можно передавать один
16
17
  элемент или iterable. Все полученные источники опрашиваются при probe: нерабочие
17
18
  отбрасываются с логированием причины, а источники с чужим владельцем — по id
@@ -19,10 +20,10 @@ TokenSource), cookies (словари с p и remixsid) и sources (готовы
19
20
 
20
21
  HTTP: клиент работает с протоколом HttpClient (асинхронные get/post); httpx —
21
22
  опциональная зависимость, создаётся лениво, если свой клиент не передан.
22
- http_client / base_api_url / v задаются у клиента и могут быть переопределены
23
- для отдельного запроса (call); method_versions задаёт версии по умолчанию
24
- отдельных методов (messages.send и messages.edit используют v=5.281). Явный v вызова
25
- приоритетнее.
23
+ user_agent при передаче заменяет User-Agent в заголовках HTTP-клиента.
24
+ http_client / base_api_url / v задаются у клиента и могут быть переопределены для
25
+ отдельного запроса (call); method_versions задаёт версии по умолчанию отдельных
26
+ методов (messages.send и messages.edit используют v=5.281). Явный v вызова приоритетнее.
26
27
 
27
28
  Очередь и батчинг: call() фиксирует маршрут запроса (_Queued: кандидаты, HTTP-
28
29
  настройки и текущий источник) и кладёт его в очередь; PendingApiCall ждёт
@@ -50,6 +51,7 @@ import time
50
51
  from collections import deque
51
52
  from collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Mapping
52
53
  from dataclasses import dataclass
54
+ from pathlib import Path
53
55
  from typing import Any, Self, overload
54
56
 
55
57
  from ..models.categories import APICategories
@@ -60,7 +62,7 @@ from ..models.formatting import (
60
62
  )
61
63
  from . import pagination
62
64
  from .errors import VKError, VKTimeoutError, VKTransportError, build_error
63
- from .http import API_HOST, API_VERSION, HttpClient, create_http_client
65
+ from .http import API_HOST, API_VERSION, HttpClient, create_http_client, override_user_agent
64
66
  from .resolve import (
65
67
  DEFAULT_RESOLVE_CACHE_SIZE,
66
68
  DEFAULT_RESOLVE_CACHE_TTL,
@@ -74,6 +76,7 @@ from .resolve import (
74
76
  from .storage import Store
75
77
  from .tokens import (
76
78
  ALL_SCOPES,
79
+ PasswordCookieSource,
77
80
  StaticTokenSource,
78
81
  TokenSource,
79
82
  TokenSourceError,
@@ -103,7 +106,7 @@ _FORMAT_METHODS: frozenset[str] = frozenset({"messages.send", "messages.edit"})
103
106
  CookieMap = Mapping[str, str]
104
107
  TokenInput = str | TokenSource
105
108
  TokensInput = TokenInput | Iterable[TokenInput] | None
106
- CookiesInput = CookieMap | Iterable[CookieMap] | None
109
+ CookiesInput = CookieMap | Iterable[CookieMap] | str | Path | None
107
110
  SourcesInput = TokenSource | Iterable[TokenSource] | None
108
111
  CaptchaHandler = Callable[[VKError], "Awaitable[Mapping[str, str] | None] | Mapping[str, str] | None"]
109
112
  """Обработчик капчи: получает VKCaptchaError, возвращает лишние параметры запроса."""
@@ -244,15 +247,43 @@ def _as_list(value: Any) -> list[Any]:
244
247
 
245
248
 
246
249
  def _iter_cookie_dicts(cookies: CookiesInput) -> list[dict[str, str]]:
247
- """Нормализует cookies (один словарь или iterable) и проверяет p + remixsid."""
250
+ """Читает и нормализует cookies из словаря, iterable или JSON-файла."""
248
251
  if cookies is None:
249
252
  return []
250
- items = [cookies] if isinstance(cookies, Mapping) else list(cookies)
253
+
254
+ payload: Any = cookies
255
+ if isinstance(cookies, (str, Path)):
256
+ path = Path(cookies)
257
+ try:
258
+ payload = json.loads(path.read_text(encoding="utf-8"))
259
+ except (OSError, ValueError) as exc:
260
+ raise TokenSourceError(f"не удалось прочитать JSON cookies из {path}") from exc
261
+
262
+ if isinstance(payload, Mapping):
263
+ if "cookies" in payload and not all(
264
+ name in payload for name in ("p", "remixsid")
265
+ ):
266
+ items = payload["cookies"]
267
+ if not isinstance(items, list):
268
+ raise TokenSourceError('cookies: поле "cookies" в JSON должно быть списком')
269
+ else:
270
+ items = [payload]
271
+ elif isinstance(payload, Iterable) and not isinstance(payload, (str, bytes)):
272
+ items = list(payload)
273
+ else:
274
+ raise TokenSourceError(
275
+ "cookies: ожидался словарь, список cookie-записей или JSON-файл"
276
+ )
277
+
251
278
  result: list[dict[str, str]] = []
252
279
  for item in items:
253
280
  if not isinstance(item, Mapping):
254
- raise TypeError(f"cookies: ожидался словарь, получено {item!r}")
281
+ raise TokenSourceError("cookies: каждая запись должна быть JSON-объектом")
255
282
  value = {str(key): str(val) for key, val in item.items()}
283
+ if "p" in item and "remixsid" in item:
284
+ # Метаданные записей vkqr — не cookie и не должны уходить HTTP-клиенту.
285
+ for name in ("created_at", "user_id", "_vkx"):
286
+ value.pop(name, None)
256
287
  validate_web_cookies(value)
257
288
  result.append(value)
258
289
  return result
@@ -487,8 +518,9 @@ class VKClient(APICategories):
487
518
  """Клиент VK API: источники токенов одного владельца, очередь и батчинг execute.
488
519
 
489
520
  Источники задаются необязательными tokens (строки/TokenSource), cookies
490
- (словари с p и remixsid; проверяются сразу) и sources (готовые TokenSource);
491
- tokens/cookies превращаются в источники автоматически, можно смешивать.
521
+ (словари с p и remixsid, JSON-структура или путь к JSON-файлу), логином/паролем
522
+ или готовыми sources (TokenSource). Источники проверяются при probe; клиент
523
+ сохраняет привязку к владельцу первого рабочего источника.
492
524
  Конструктор не обращается к сети. probe() (автоматически при первом call())
493
525
  опрашивает все источники: нерабочие отбрасываются с логированием причины,
494
526
  клиент привязывается к владельцу первого валидного токена (user_id
@@ -504,11 +536,14 @@ class VKClient(APICategories):
504
536
  *,
505
537
  logger: logging.Logger | None = None,
506
538
  http_client: HttpClient | None = None,
539
+ user_agent: str | None = None,
507
540
  base_api_url: str = API_HOST,
508
541
  v: str = API_VERSION,
509
542
  method_versions: Mapping[str, str] | None = None,
510
543
  lang: int = 0,
511
544
  store: Store | None = None,
545
+ login: str | None = None,
546
+ password: str | None = None,
512
547
  interval: float | None = None,
513
548
  max_retries: int = DEFAULT_MAX_RETRIES,
514
549
  retry_backoff: float = DEFAULT_RETRY_BACKOFF,
@@ -534,18 +569,38 @@ class VKClient(APICategories):
534
569
  raise ValueError("resolve_cache_ttl: ttl кеша не может быть отрицательным")
535
570
  if resolve_cache_size < 0:
536
571
  raise ValueError("resolve_cache_size: размер кеша не может быть отрицательным")
572
+ if login is not None and not login.strip():
573
+ raise ValueError("login: укажите логин VK")
574
+ if password is not None and login is None:
575
+ raise ValueError("password: для пароля требуется login")
576
+ if password == "":
577
+ raise ValueError("password: пароль не может быть пустым")
537
578
  cookie_dicts = _iter_cookie_dicts(cookies)
538
579
  token_sources = _token_sources(tokens, store=store)
539
580
  explicit_sources = _source_objects(sources)
540
581
  self._own_http = http_client is None
541
582
  self._http: HttpClient = http_client or create_http_client()
583
+ if user_agent is not None:
584
+ override_user_agent(self._http, user_agent)
542
585
  self._base_api_url = base_api_url.rstrip("/")
586
+ self._password_source = (
587
+ PasswordCookieSource(
588
+ login,
589
+ password,
590
+ http=http_client,
591
+ store=store,
592
+ user_agent=user_agent,
593
+ )
594
+ if login is not None
595
+ else None
596
+ )
543
597
  built: list[TokenSource] = [
544
598
  *token_sources,
545
599
  *(
546
600
  WebCookieSource(item, http=self._http, store=store, source=f"web{index}")
547
601
  for index, item in enumerate(cookie_dicts)
548
602
  ),
603
+ *([self._password_source] if self._password_source is not None else []),
549
604
  *explicit_sources,
550
605
  ]
551
606
  self._entries = [TokenSourceEntry(s) for s in built]
@@ -1612,6 +1667,8 @@ class VKClient(APICategories):
1612
1667
  error = VKError("клиент закрыт", client=self)
1613
1668
  while self._queue:
1614
1669
  self._queue.popleft().request.set_error(error)
1670
+ if self._password_source is not None:
1671
+ await self._password_source.aclose()
1615
1672
  if self._own_http:
1616
1673
  await self._http.aclose()
1617
1674
 
@@ -8,6 +8,7 @@ status_code / text / json(). httpx — опциональная зависимо
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
+ from collections.abc import MutableMapping
11
12
  from typing import Any, Protocol, cast, runtime_checkable
12
13
 
13
14
  DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:159.0) Gecko/20100101 Firefox/159.0"
@@ -43,6 +44,20 @@ class HttpClient(Protocol):
43
44
  async def aclose(self) -> None: ...
44
45
 
45
46
 
47
+ def override_user_agent(http_client: HttpClient, user_agent: str) -> None:
48
+ """Переопределить User-Agent в заголовках HTTP-клиента."""
49
+ headers = getattr(http_client, "headers", None)
50
+ if not isinstance(headers, MutableMapping):
51
+ raise TypeError(
52
+ "user_agent: HTTP-клиент должен предоставлять изменяемые заголовки headers"
53
+ )
54
+
55
+ for name in tuple(headers):
56
+ if isinstance(name, str) and name.casefold() == "user-agent":
57
+ del headers[name]
58
+ headers["User-Agent"] = user_agent
59
+
60
+
46
61
  def create_http_client() -> HttpClient:
47
62
  """HTTP-клиент по умолчанию; httpx импортируется лениво (опциональная зависимость)."""
48
63
  try: