tweepy-self 1.6.3__py3-none-any.whl → 1.10.0b9__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- tweepy_self-1.10.0b9.dist-info/METADATA +303 -0
- tweepy_self-1.10.0b9.dist-info/RECORD +23 -0
- twitter/__init__.py +15 -5
- twitter/_capsolver/__init__.py +0 -0
- twitter/_capsolver/core/__init__.py +0 -0
- twitter/_capsolver/core/base.py +227 -0
- twitter/_capsolver/core/config.py +36 -0
- twitter/_capsolver/core/enum.py +66 -0
- twitter/_capsolver/core/serializer.py +85 -0
- twitter/_capsolver/fun_captcha.py +260 -0
- twitter/account.py +17 -13
- twitter/base/__init__.py +2 -2
- twitter/base/client.py +4 -4
- twitter/client.py +744 -382
- twitter/errors.py +14 -7
- twitter/models.py +147 -50
- twitter/utils/__init__.py +2 -0
- twitter/utils/html.py +6 -2
- twitter/utils/other.py +13 -0
- tweepy_self-1.6.3.dist-info/METADATA +0 -218
- tweepy_self-1.6.3.dist-info/RECORD +0 -16
- {tweepy_self-1.6.3.dist-info → tweepy_self-1.10.0b9.dist-info}/WHEEL +0 -0
@@ -0,0 +1,303 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: tweepy-self
|
3
|
+
Version: 1.10.0b9
|
4
|
+
Summary: Twitter (selfbot) for Python!
|
5
|
+
Home-page: https://github.com/alenkimov/tweepy-self
|
6
|
+
Author: Alen
|
7
|
+
Author-email: alen.kimov@gmail.com
|
8
|
+
Requires-Python: >=3.11,<4.0
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
12
|
+
Requires-Dist: aiohttp (>=3.9,<4.0)
|
13
|
+
Requires-Dist: beautifulsoup4 (>=4,<5)
|
14
|
+
Requires-Dist: better-proxy (>=1.1,<2.0)
|
15
|
+
Requires-Dist: curl_cffi (==0.6.2)
|
16
|
+
Requires-Dist: loguru (>=0.7,<0.8)
|
17
|
+
Requires-Dist: lxml (>=5,<6)
|
18
|
+
Requires-Dist: pydantic (>=2,<3)
|
19
|
+
Requires-Dist: pyotp (>=2,<3)
|
20
|
+
Requires-Dist: requests (>=2,<3)
|
21
|
+
Requires-Dist: tenacity (>=8,<9)
|
22
|
+
Requires-Dist: yarl (>=1,<2)
|
23
|
+
Project-URL: Repository, https://github.com/alenkimov/tweepy-self
|
24
|
+
Project-URL: Source, https://github.com/alenkimov/tweepy-self
|
25
|
+
Description-Content-Type: text/markdown
|
26
|
+
|
27
|
+
# Tweepy-self
|
28
|
+
[![Telegram channel](https://img.shields.io/endpoint?url=https://runkit.io/damiankrawczyk/telegram-badge/branches/master?url=https://t.me/cum_insider)](https://t.me/cum_insider)
|
29
|
+
[![PyPI version info](https://img.shields.io/pypi/v/tweepy-self.svg)](https://pypi.python.org/pypi/tweepy-self)
|
30
|
+
[![PyPI supported Python versions](https://img.shields.io/pypi/pyversions/tweepy-self.svg)](https://pypi.python.org/pypi/tweepy-self)
|
31
|
+
[![PyPI downloads per month](https://img.shields.io/pypi/dm/tweepy-self.svg)](https://pypi.python.org/pypi/tweepy-self)
|
32
|
+
|
33
|
+
A modern, easy to use, feature-rich, and async ready API wrapper for Twitter's user API written in Python.
|
34
|
+
|
35
|
+
More libraries of the family:
|
36
|
+
- [better-proxy](https://github.com/alenkimov/better_proxy)
|
37
|
+
- [better-web3](https://github.com/alenkimov/better_web3)
|
38
|
+
|
39
|
+
Отдельное спасибо [Кузнице Ботов](https://t.me/bots_forge) за код для авторизации и разморозки! Подписывайтесь на их Telegram :)
|
40
|
+
|
41
|
+
## Key Features
|
42
|
+
- Modern Pythonic API using async and await.
|
43
|
+
- Prevents user account automation detection.
|
44
|
+
|
45
|
+
## Installing
|
46
|
+
```bash
|
47
|
+
pip install tweepy-self
|
48
|
+
```
|
49
|
+
|
50
|
+
## Example
|
51
|
+
```python
|
52
|
+
import asyncio
|
53
|
+
import twitter
|
54
|
+
|
55
|
+
twitter_account = twitter.Account(auth_token="auth_token")
|
56
|
+
|
57
|
+
async def main():
|
58
|
+
async with twitter.Client(twitter_account) as twitter_client:
|
59
|
+
print(f"Logged in as @{twitter_account.username} (id={twitter_account.id})")
|
60
|
+
tweet = await twitter_client.tweet("Hello tweepy-self! <3")
|
61
|
+
print(tweet)
|
62
|
+
|
63
|
+
if __name__ == "__main__":
|
64
|
+
asyncio.run(main())
|
65
|
+
```
|
66
|
+
|
67
|
+
## Документация
|
68
|
+
### Некоторые истины
|
69
|
+
Имена пользователей нужно передавать БЕЗ знака `@`.
|
70
|
+
Чтобы наверняка убрать этот знак можно передать имя пользователя в функцию `twitter.utils.remove_at_sign()`
|
71
|
+
|
72
|
+
Automating user accounts is against the Twitter ToS. This library is a proof of concept and I cannot recommend using it. Do so at your own risk
|
73
|
+
|
74
|
+
### Как включить логирование
|
75
|
+
```python
|
76
|
+
import sys
|
77
|
+
from loguru import logger
|
78
|
+
|
79
|
+
logger.remove()
|
80
|
+
logger.add(sys.stdout, level="INFO")
|
81
|
+
logger.enable("twitter")
|
82
|
+
```
|
83
|
+
|
84
|
+
`level="DEBUG"` позволяет увидеть информацию обо всех запросах.
|
85
|
+
|
86
|
+
### Аккаунт
|
87
|
+
После любого взаимодействия с Twitter устанавливается статус аккаунта:
|
88
|
+
- `UNKNOWN` - Статус аккаунта не установлен. Это статус по умолчанию.
|
89
|
+
- `BAD_TOKEN` - Неверный или мертвый токен.
|
90
|
+
- `SUSPENDED` - Действие учетной записи приостановлено. Тем не менее возможен запрос данных, а также авторизация через OAuth и OAuth2.
|
91
|
+
- `LOCKED` - Учетная запись заморожена (лок). Для разморозки (анлок) требуется прохождение капчи (funcaptcha).
|
92
|
+
- `CONSENT_LOCKED` - Учетная запись заморожена (лок). Условия для разморозки неизвестны.
|
93
|
+
- `GOOD` - Аккаунт в порядке.
|
94
|
+
|
95
|
+
Не каждое взаимодействие с Twitter достоверно определяет статус аккаунта.
|
96
|
+
Например, простой запрос данных об аккаунте честно вернет данные, даже если действие вашей учетной записи приостановлено.
|
97
|
+
|
98
|
+
Для достоверной установки статуса аккаунта используйте метод `Client.establish_status()`
|
99
|
+
|
100
|
+
### Настройка клиента
|
101
|
+
Класс `twitter.Client` может быть сконфигурирован перед работой. Он принимает в себя следующие параметры:
|
102
|
+
- `wait_on_rate_limit` Если включено, то при достижении Rate Limit будет ждать, вместо того, чтобы выбрасывать исключение. Включено по умолчанию.
|
103
|
+
- `capsolver_api_key` API ключ сервиса [CapSolver](https://dashboard.capsolver.com/passport/register?inviteCode=m-aE3NeBGZLU). Нужен для автоматической разморозки аккаунта.
|
104
|
+
- `max_unlock_attempts` Максимальное количество попыток разморозки аккаунта. По умолчанию: 5.
|
105
|
+
- `auto_relogin` Если включено, то при невалидном токене (`BAD_TOKEN`) и предоставленных данных для авторизации (имя пользователя, пароль и totp_secret) будет произведен автоматический релогин (замена токена). Включено по умолчанию.
|
106
|
+
- `update_account_info_on_startup` Если включено, то на старте будет автоматически запрошена информация об аккаунте. Включено по умолчанию.
|
107
|
+
- `**session_kwargs` Любые параметры, которые может принимать сессия `curl_cffi.requests.AsyncSession`. Например, можно передать параметр `proxy`.
|
108
|
+
|
109
|
+
Пример настройки клиента:
|
110
|
+
```python
|
111
|
+
async with twitter.Client(
|
112
|
+
twitter_account,
|
113
|
+
capsolver_api_key="CAP-00000000000000000000000000000000",
|
114
|
+
proxy="http://login:password@ip:port", # Можно передавать в любом формате, так как используется библиотека better_proxy
|
115
|
+
) as twitter_client:
|
116
|
+
...
|
117
|
+
```
|
118
|
+
|
119
|
+
### Доступные методы
|
120
|
+
Список всех методов.
|
121
|
+
|
122
|
+
#### Запрос информации о собственном аккаунте
|
123
|
+
```python
|
124
|
+
twitter_client.update_account_info()
|
125
|
+
print(twitter_client.account)
|
126
|
+
```
|
127
|
+
|
128
|
+
#### Запрос пользователя по username или по ID
|
129
|
+
|
130
|
+
```python
|
131
|
+
bro = twitter_client.request_user_by_username(bro_username)
|
132
|
+
bro = twitter_client.request_user_by_id(bro_id)
|
133
|
+
bros = twitter_client.request_users_by_ids([bro1_id, bro2_id, ...])
|
134
|
+
```
|
135
|
+
|
136
|
+
#### Загрузка изображения на сервер, смена аватарки и баннера
|
137
|
+
```python
|
138
|
+
image = open("image.png", "rb").read()
|
139
|
+
media = await twitter_client.upload_image(image)
|
140
|
+
avatar_image_url = await twitter_client.update_profile_avatar(media.id)
|
141
|
+
banner_image_url = await twitter_client.update_profile_banner(media.id)
|
142
|
+
```
|
143
|
+
|
144
|
+
#### Изменения данных профиля
|
145
|
+
```python
|
146
|
+
await twitter_client.update_birthdate(day=1, month=12, year=2000)
|
147
|
+
await twitter_client.update_profile( # Locks account!
|
148
|
+
name="New Name",
|
149
|
+
description="New description",
|
150
|
+
location="New York",
|
151
|
+
website="https://github.com/alenkimov/tweepy-self",
|
152
|
+
)
|
153
|
+
```
|
154
|
+
|
155
|
+
#### Включение TOTP (2FA)
|
156
|
+
```python
|
157
|
+
if await twitter_client.totp_is_enabled():
|
158
|
+
print(f"TOTP уже включен.")
|
159
|
+
return
|
160
|
+
|
161
|
+
await twitter_client.enable_totp()
|
162
|
+
```
|
163
|
+
|
164
|
+
#### Логин, если включен TOTP (2FA)
|
165
|
+
```python
|
166
|
+
import twitter
|
167
|
+
|
168
|
+
twitter_account = twitter.Account(auth_token="...", username="...", password="...", totp_secret="...")
|
169
|
+
await twitter_client.login()
|
170
|
+
print(f"Logged in! New auth_token: {twitter_account.auth_token}")
|
171
|
+
```
|
172
|
+
|
173
|
+
#### Смена имени пользователя и пароля
|
174
|
+
```python
|
175
|
+
twitter_account = twitter.Account("auth_token", password="password")
|
176
|
+
...
|
177
|
+
await twitter_client.change_username("new_username")
|
178
|
+
await twitter_client.request_user()
|
179
|
+
print(f"New username: {twitter_account.username}")
|
180
|
+
|
181
|
+
await twitter_client.change_password("new_password")
|
182
|
+
print(f"New password: {twitter_account.password}")
|
183
|
+
print(f"New auth_token: {twitter_account.auth_token}")
|
184
|
+
```
|
185
|
+
|
186
|
+
#### Авторизация с OAuth
|
187
|
+
```python
|
188
|
+
auth_code = await twitter_client.oauth(oauth_token, **oauth_params)
|
189
|
+
```
|
190
|
+
|
191
|
+
#### Авторизация с OAuth2
|
192
|
+
```python
|
193
|
+
# Изучите запросы сервиса и найдите подобные данные для авторизации (привязки):
|
194
|
+
oauth2_data = {
|
195
|
+
'response_type': 'code',
|
196
|
+
'client_id': 'TjFVQm52ZDFGWEtNT0tKaktaSWU6MTpjaQ',
|
197
|
+
'redirect_uri': 'https://waitlist.lens.xyz/tw/',
|
198
|
+
'scope': 'users.read tweet.read offline.access',
|
199
|
+
'state': 'state', # Может быть как статичным, так и динамическим.
|
200
|
+
'code_challenge': 'challenge',
|
201
|
+
'code_challenge_method': 'plain'
|
202
|
+
}
|
203
|
+
|
204
|
+
auth_code = await twitter_client.oauth2(**oauth2_data)
|
205
|
+
# Передайте код авторизации (привязки) сервису.
|
206
|
+
# Сервис также может потребовать state, если он динамический.
|
207
|
+
```
|
208
|
+
|
209
|
+
#### Отправка сообщения:
|
210
|
+
```python
|
211
|
+
bro = await twitter_client.request_user("bro_username")
|
212
|
+
await twitter_client.send_message(bro.id, "I love you!")
|
213
|
+
```
|
214
|
+
|
215
|
+
#### Запрос входящих сообщений:
|
216
|
+
```python
|
217
|
+
messages = await twitter_client.request_messages()
|
218
|
+
for message in messages:
|
219
|
+
message_data = message["message_data"]
|
220
|
+
recipient_id = message_data["recipient_id"]
|
221
|
+
sender_id = message_data["sender_id"]
|
222
|
+
text = message_data["text"]
|
223
|
+
print(f"[id {sender_id}] -> [id {recipient_id}]: {text}")
|
224
|
+
```
|
225
|
+
|
226
|
+
Так как мне почти не приходилось работать с сообщениями, я еще не сделал для этого удобных моделей.
|
227
|
+
Поэтому приходится работать со словарем.
|
228
|
+
|
229
|
+
#### Пост (твит)
|
230
|
+
```python
|
231
|
+
tweet = await twitter_client.tweet("I love you tweepy-self! <3")
|
232
|
+
print(f"Любовь выражена! Tweet id: {tweet.id}")
|
233
|
+
```
|
234
|
+
|
235
|
+
#### Лайк, репост (ретвит), коммент (реплай)
|
236
|
+
```python
|
237
|
+
# Лайк
|
238
|
+
print(f"Tweet {tweet_id} is liked: {await twitter_client.like(tweet_id)}")
|
239
|
+
|
240
|
+
# Репост (ретвит)
|
241
|
+
print(f"Tweet {tweet_id} is retweeted. Tweet id: {await twitter_client.repost(tweet_id)}")
|
242
|
+
|
243
|
+
# Коммент (реплай)
|
244
|
+
print(f"Tweet {tweet_id} is replied. Reply id: {await twitter_client.reply(tweet_id, 'tem razão')}")
|
245
|
+
```
|
246
|
+
|
247
|
+
#### Цитата
|
248
|
+
```python
|
249
|
+
tweet_url = 'https://twitter.com/CreamIce_Cone/status/1691735090529976489'
|
250
|
+
# Цитата (Quote tweet)
|
251
|
+
quote_tweet_id = await twitter_client.quote(tweet_url, 'oh....')
|
252
|
+
print(f"Quoted! Tweet id: {quote_tweet_id}")
|
253
|
+
```
|
254
|
+
|
255
|
+
#### Подписка и отписка
|
256
|
+
```python
|
257
|
+
# Подписываемся на Илона Маска
|
258
|
+
print(f"@{elonmusk.username} is followed: {await twitter_client.follow(elonmusk.id)}")
|
259
|
+
|
260
|
+
# Отписываемся от Илона Маска
|
261
|
+
print(f"@{elonmusk.username} is unfollowed: {await twitter_client.unfollow(elonmusk.id)}")
|
262
|
+
```
|
263
|
+
|
264
|
+
#### Закрепление твита
|
265
|
+
```python
|
266
|
+
pinned = await twitter_client.pin_tweet(tweet_id)
|
267
|
+
print(f"Tweet is pined: {pinned}")
|
268
|
+
```
|
269
|
+
|
270
|
+
#### Запрос своих и чужих подписчиков
|
271
|
+
```python
|
272
|
+
|
273
|
+
followers = await twitter_client.request_followers()
|
274
|
+
print("Твои подписчики:")
|
275
|
+
for user in followers:
|
276
|
+
print(user)
|
277
|
+
|
278
|
+
followings = await twitter_client.request_followings()
|
279
|
+
print(f"Ты подписан на:")
|
280
|
+
for user in followings:
|
281
|
+
print(user)
|
282
|
+
|
283
|
+
bro_followers = await twitter_client.request_followers(bro_id)
|
284
|
+
print(f"Подписчики твоего бро (id={bro_id}):")
|
285
|
+
for user in bro_followers:
|
286
|
+
print(user)
|
287
|
+
|
288
|
+
bro_followings = await twitter_client.request_followings(bro_id)
|
289
|
+
print(f"На твоего бро (id={bro_id}) подписаны:")
|
290
|
+
for user in bro_followings:
|
291
|
+
print(user)
|
292
|
+
```
|
293
|
+
|
294
|
+
#### Голосование
|
295
|
+
```python
|
296
|
+
vote_data = await twitter_client.vote(tweet_id, card_id, choice_number)
|
297
|
+
votes_count = vote_data["card"]["binding_values"]["choice1_count"]["string_value"]
|
298
|
+
print(f"Votes: {votes_count}")
|
299
|
+
```
|
300
|
+
|
301
|
+
Так как мне почти не приходилось работать с голосованиями, я еще не сделал для этого удобных моделей.
|
302
|
+
Поэтому приходится работать со словарем.
|
303
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
twitter/__init__.py,sha256=-CmcPdm1z-OkG8LkJVe75PwdYKBqBfMpD9WdoXcnGuc,732
|
2
|
+
twitter/_capsolver/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
+
twitter/_capsolver/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
|
+
twitter/_capsolver/core/base.py,sha256=In3qDLgRh1z1UZLaLFgYcDEdnqW3d62PVzgEjU2S4BU,8883
|
5
|
+
twitter/_capsolver/core/config.py,sha256=8_eXT6N2hBheN2uCMNhqk8tLZRJjLDTYLK208fqIkhM,1054
|
6
|
+
twitter/_capsolver/core/enum.py,sha256=ivfAEN6jrg3iaq5C3H7CuRqsvOloX1b8lF8cLa3zaiY,1741
|
7
|
+
twitter/_capsolver/core/serializer.py,sha256=xPEUIPgytuw2wM1ubTY3RMhJGVyp_d3bokPTx0BjF0c,2602
|
8
|
+
twitter/_capsolver/fun_captcha.py,sha256=VVbTmn08cGnvPMGdJmPxaLfAIPxyA68oTSAyEL8RWnU,10974
|
9
|
+
twitter/account.py,sha256=joAB5Zw-Le5E3kOZ-1nb4DPGlTqWYv2Vs6gJ3cwu7is,3175
|
10
|
+
twitter/base/__init__.py,sha256=Q2ko0HeOS5tiBnDVKxxaZYetwRR3YXJ67ujL3oThGd4,141
|
11
|
+
twitter/base/client.py,sha256=J_iL4ZGfwTbZ2gpjtFCbBxNgt7weJ55EeMGzYsLtjf4,500
|
12
|
+
twitter/base/session.py,sha256=JFPS-9Qae1iY3NfNcywxvWWmRDijaU_Rjs3WaQ00iFA,2071
|
13
|
+
twitter/client.py,sha256=VZ9cymxz1KMSaqVTTFfCfuC1c5xZHdRUOD0XENSf53w,75555
|
14
|
+
twitter/enums.py,sha256=-OH6Ibxarq5qt4E2AhkProVawcEyIf5YG_h_G5xiV9Y,270
|
15
|
+
twitter/errors.py,sha256=oNa0Neos80ZK4-0FBzqgxXonH564qFnoN-kavHalfR4,5274
|
16
|
+
twitter/models.py,sha256=CrGb3dvA0U4PfPTkUtuprPKXpqkLpM8AR_-De4D3efM,5677
|
17
|
+
twitter/utils/__init__.py,sha256=usxpfcRQ7zxTTgZ-i425tT7hIz73Pwh9FDj4t6O3dYg,663
|
18
|
+
twitter/utils/file.py,sha256=Sz2KEF9DnL04aOP1XabuMYMMF4VR8dJ_KWMEVvQ666Y,1120
|
19
|
+
twitter/utils/html.py,sha256=nrOJw0vUKfBaHgFaQSQIdXfvfZ8mdu84MU_s46kJTJ4,2087
|
20
|
+
twitter/utils/other.py,sha256=9RIYF2AMdmNKIwClG3jBP7zlvxZPEgYfuHaIiOhURzM,1061
|
21
|
+
tweepy_self-1.10.0b9.dist-info/METADATA,sha256=DUbx5Sw46Hy6B1ZIsQVOnFx3BTjH1qZ0yplMDTJ7njE,13153
|
22
|
+
tweepy_self-1.10.0b9.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
23
|
+
tweepy_self-1.10.0b9.dist-info/RECORD,,
|
twitter/__init__.py
CHANGED
@@ -6,14 +6,23 @@ A basic wrapper for the Twitter user API.
|
|
6
6
|
"""
|
7
7
|
|
8
8
|
from .client import Client
|
9
|
-
from .account import
|
10
|
-
|
9
|
+
from .account import (
|
10
|
+
Account,
|
11
|
+
AccountStatus,
|
12
|
+
load_accounts_from_file,
|
13
|
+
extract_accounts_to_file,
|
14
|
+
)
|
15
|
+
from .models import Tweet, User, Media, Image
|
11
16
|
from . import errors, utils
|
12
17
|
|
13
18
|
__all__ = [
|
14
19
|
"Client",
|
15
20
|
"Account",
|
16
21
|
"AccountStatus",
|
22
|
+
"Tweet",
|
23
|
+
"User",
|
24
|
+
"Media",
|
25
|
+
"Image",
|
17
26
|
"utils",
|
18
27
|
"errors",
|
19
28
|
"load_accounts_from_file",
|
@@ -22,9 +31,10 @@ __all__ = [
|
|
22
31
|
|
23
32
|
|
24
33
|
import warnings
|
34
|
+
|
25
35
|
# HACK: Ignore event loop warnings from curl_cffi
|
26
|
-
warnings.filterwarnings(
|
36
|
+
warnings.filterwarnings("ignore", module="curl_cffi")
|
27
37
|
|
38
|
+
from loguru import logger
|
28
39
|
|
29
|
-
|
30
|
-
config.APP_ID = "6F895B2F-F454-44D1-8FE0-77ACAD3DBDC8"
|
40
|
+
logger.disable("twitter")
|
File without changes
|
File without changes
|
@@ -0,0 +1,227 @@
|
|
1
|
+
import time
|
2
|
+
import asyncio
|
3
|
+
import logging
|
4
|
+
from typing import Any, Dict, Type
|
5
|
+
from urllib import parse
|
6
|
+
|
7
|
+
import aiohttp
|
8
|
+
import requests
|
9
|
+
from pydantic import BaseModel
|
10
|
+
from requests.adapters import HTTPAdapter
|
11
|
+
|
12
|
+
from .enum import ResponseStatusEnm, EndpointPostfixEnm
|
13
|
+
from .config import RETRIES, REQUEST_URL, VALID_STATUS_CODES, attempts_generator
|
14
|
+
from .serializer import (
|
15
|
+
CaptchaOptionsSer,
|
16
|
+
CaptchaResponseSer,
|
17
|
+
RequestCreateTaskSer,
|
18
|
+
RequestGetTaskResultSer,
|
19
|
+
)
|
20
|
+
|
21
|
+
|
22
|
+
class BaseCaptcha:
|
23
|
+
"""
|
24
|
+
Basic Captcha solving class
|
25
|
+
|
26
|
+
Args:
|
27
|
+
api_key: Capsolver API key
|
28
|
+
captcha_type: Captcha type name, like `ReCaptchaV2Task` and etc.
|
29
|
+
sleep_time: The waiting time between requests to get the result of the Captcha
|
30
|
+
request_url: API address for sending requests
|
31
|
+
"""
|
32
|
+
|
33
|
+
def __init__(
|
34
|
+
self,
|
35
|
+
api_key: str,
|
36
|
+
sleep_time: int = 5,
|
37
|
+
request_url: str = REQUEST_URL,
|
38
|
+
**kwargs,
|
39
|
+
):
|
40
|
+
# assign args to validator
|
41
|
+
self.__params = CaptchaOptionsSer(**locals())
|
42
|
+
self.__request_url = request_url
|
43
|
+
|
44
|
+
# prepare session
|
45
|
+
self.__session = requests.Session()
|
46
|
+
self.__session.mount("http://", HTTPAdapter(max_retries=RETRIES))
|
47
|
+
self.__session.mount("https://", HTTPAdapter(max_retries=RETRIES))
|
48
|
+
|
49
|
+
def _prepare_create_task_payload(self, serializer: Type[BaseModel], create_params: Dict[str, Any] = None) -> None:
|
50
|
+
"""
|
51
|
+
Method prepare `createTask` payload
|
52
|
+
|
53
|
+
Args:
|
54
|
+
serializer: Serializer for task creation
|
55
|
+
create_params: Parameters for task creation payload
|
56
|
+
|
57
|
+
Examples:
|
58
|
+
|
59
|
+
>>> self._prepare_create_task_payload(serializer=PostRequestSer, create_params={})
|
60
|
+
|
61
|
+
"""
|
62
|
+
self.task_payload = serializer(clientKey=self.__params.api_key)
|
63
|
+
# added task params to payload
|
64
|
+
self.task_payload.task = {**create_params} if create_params else {}
|
65
|
+
|
66
|
+
def __enter__(self):
|
67
|
+
return self
|
68
|
+
|
69
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
70
|
+
if exc_type:
|
71
|
+
return False
|
72
|
+
return True
|
73
|
+
|
74
|
+
async def __aenter__(self):
|
75
|
+
return self
|
76
|
+
|
77
|
+
async def __aexit__(self, exc_type, exc_value, traceback):
|
78
|
+
if exc_type:
|
79
|
+
return False
|
80
|
+
return True
|
81
|
+
|
82
|
+
"""
|
83
|
+
Sync part
|
84
|
+
"""
|
85
|
+
|
86
|
+
def _processing_captcha(
|
87
|
+
self, create_params: dict, serializer: Type[BaseModel] = RequestCreateTaskSer
|
88
|
+
) -> CaptchaResponseSer:
|
89
|
+
self._prepare_create_task_payload(serializer=serializer, create_params=create_params)
|
90
|
+
self.created_task_data = CaptchaResponseSer(**self._create_task())
|
91
|
+
|
92
|
+
# if task created and ready - return result
|
93
|
+
if self.created_task_data.status == ResponseStatusEnm.Ready.value:
|
94
|
+
return self.created_task_data
|
95
|
+
# if captcha is not ready but task success created - waiting captcha result
|
96
|
+
elif self.created_task_data.errorId == 0:
|
97
|
+
return self._get_result()
|
98
|
+
return self.created_task_data
|
99
|
+
|
100
|
+
def _create_task(self, url_postfix: str = EndpointPostfixEnm.CREATE_TASK.value) -> dict:
|
101
|
+
"""
|
102
|
+
Function send SYNC request to service and wait for result
|
103
|
+
"""
|
104
|
+
try:
|
105
|
+
resp = self.__session.post(
|
106
|
+
parse.urljoin(self.__request_url, url_postfix), json=self.task_payload.dict(exclude_none=True)
|
107
|
+
)
|
108
|
+
if resp.status_code in VALID_STATUS_CODES:
|
109
|
+
return resp.json()
|
110
|
+
else:
|
111
|
+
raise ValueError(resp.raise_for_status())
|
112
|
+
except Exception as error:
|
113
|
+
logging.exception(error)
|
114
|
+
raise
|
115
|
+
|
116
|
+
def _get_result(self, url_postfix: str = EndpointPostfixEnm.GET_TASK_RESULT.value) -> CaptchaResponseSer:
|
117
|
+
"""
|
118
|
+
Method send SYNC request to service and wait for result
|
119
|
+
"""
|
120
|
+
# initial waiting
|
121
|
+
time.sleep(self.__params.sleep_time)
|
122
|
+
|
123
|
+
get_result_payload = RequestGetTaskResultSer(
|
124
|
+
clientKey=self.__params.api_key, taskId=self.created_task_data.taskId
|
125
|
+
)
|
126
|
+
attempts = attempts_generator()
|
127
|
+
for _ in attempts:
|
128
|
+
try:
|
129
|
+
resp = self.__session.post(
|
130
|
+
parse.urljoin(self.__request_url, url_postfix), json=get_result_payload.dict(exclude_none=True)
|
131
|
+
)
|
132
|
+
if resp.status_code in VALID_STATUS_CODES:
|
133
|
+
result_data = CaptchaResponseSer(**resp.json())
|
134
|
+
if result_data.status in (ResponseStatusEnm.Ready, ResponseStatusEnm.Failed):
|
135
|
+
# if captcha ready\failed or have unknown status - return exist data
|
136
|
+
return result_data
|
137
|
+
else:
|
138
|
+
raise ValueError(resp.raise_for_status())
|
139
|
+
except Exception as error:
|
140
|
+
logging.exception(error)
|
141
|
+
raise
|
142
|
+
|
143
|
+
# if captcha just created or in processing now - wait
|
144
|
+
time.sleep(self.__params.sleep_time)
|
145
|
+
# default response if server is silent
|
146
|
+
return CaptchaResponseSer(
|
147
|
+
errorId=1,
|
148
|
+
errorCode="ERROR_CAPTCHA_UNSOLVABLE",
|
149
|
+
errorDescription="Captcha not recognized",
|
150
|
+
taskId=self.created_task_data.taskId,
|
151
|
+
status=ResponseStatusEnm.Failed,
|
152
|
+
)
|
153
|
+
|
154
|
+
"""
|
155
|
+
Async part
|
156
|
+
"""
|
157
|
+
|
158
|
+
async def _aio_processing_captcha(
|
159
|
+
self, create_params: dict, serializer: Type[BaseModel] = RequestCreateTaskSer
|
160
|
+
) -> CaptchaResponseSer:
|
161
|
+
self._prepare_create_task_payload(serializer=serializer, create_params=create_params)
|
162
|
+
self.created_task_data = CaptchaResponseSer(**await self._aio_create_task())
|
163
|
+
|
164
|
+
# if task created and already ready - return result
|
165
|
+
if self.created_task_data.status == ResponseStatusEnm.Ready.value:
|
166
|
+
return self.created_task_data
|
167
|
+
# if captcha is not ready but task success created - waiting captcha result
|
168
|
+
elif self.created_task_data.errorId == 0:
|
169
|
+
return await self._aio_get_result()
|
170
|
+
return self.created_task_data
|
171
|
+
|
172
|
+
async def _aio_create_task(self, url_postfix: str = EndpointPostfixEnm.CREATE_TASK.value) -> dict:
|
173
|
+
"""
|
174
|
+
Function send the ASYNC request to service and wait for result
|
175
|
+
"""
|
176
|
+
async with aiohttp.ClientSession() as session:
|
177
|
+
try:
|
178
|
+
async with session.post(
|
179
|
+
parse.urljoin(self.__request_url, url_postfix), json=self.task_payload.dict(exclude_none=True)
|
180
|
+
) as resp:
|
181
|
+
if resp.status in VALID_STATUS_CODES:
|
182
|
+
return await resp.json()
|
183
|
+
else:
|
184
|
+
raise ValueError(resp.reason)
|
185
|
+
except Exception as error:
|
186
|
+
logging.exception(error)
|
187
|
+
raise
|
188
|
+
|
189
|
+
async def _aio_get_result(self, url_postfix: str = EndpointPostfixEnm.GET_TASK_RESULT.value) -> CaptchaResponseSer:
|
190
|
+
"""
|
191
|
+
Function send the ASYNC request to service and wait for result
|
192
|
+
"""
|
193
|
+
# initial waiting
|
194
|
+
await asyncio.sleep(self.__params.sleep_time)
|
195
|
+
|
196
|
+
get_result_payload = RequestGetTaskResultSer(
|
197
|
+
clientKey=self.__params.api_key, taskId=self.created_task_data.taskId
|
198
|
+
)
|
199
|
+
attempts = attempts_generator()
|
200
|
+
async with aiohttp.ClientSession() as session:
|
201
|
+
for _ in attempts:
|
202
|
+
try:
|
203
|
+
async with session.post(
|
204
|
+
parse.urljoin(self.__request_url, url_postfix), json=get_result_payload.dict(exclude_none=True)
|
205
|
+
) as resp:
|
206
|
+
if resp.status in VALID_STATUS_CODES:
|
207
|
+
result_data = CaptchaResponseSer(**await resp.json())
|
208
|
+
if result_data.status in (ResponseStatusEnm.Ready, ResponseStatusEnm.Failed):
|
209
|
+
# if captcha ready\failed or have unknown status - return exist data
|
210
|
+
return result_data
|
211
|
+
else:
|
212
|
+
raise ValueError(resp.reason)
|
213
|
+
except Exception as error:
|
214
|
+
logging.exception(error)
|
215
|
+
raise
|
216
|
+
|
217
|
+
# if captcha just created or in processing now - wait
|
218
|
+
await asyncio.sleep(self.__params.sleep_time)
|
219
|
+
|
220
|
+
# default response if server is silent
|
221
|
+
return CaptchaResponseSer(
|
222
|
+
errorId=1,
|
223
|
+
errorCode="ERROR_CAPTCHA_UNSOLVABLE",
|
224
|
+
errorDescription="Captcha not recognized",
|
225
|
+
taskId=self.created_task_data.taskId,
|
226
|
+
status=ResponseStatusEnm.Failed,
|
227
|
+
)
|
@@ -0,0 +1,36 @@
|
|
1
|
+
from typing import Generator
|
2
|
+
|
3
|
+
from tenacity import AsyncRetrying, wait_fixed, stop_after_attempt
|
4
|
+
from requests.adapters import Retry
|
5
|
+
|
6
|
+
RETRIES = Retry(total=5, backoff_factor=0.9, status_forcelist=[500, 502, 503, 504])
|
7
|
+
ASYNC_RETRIES = AsyncRetrying(wait=wait_fixed(5), stop=stop_after_attempt(5), reraise=True)
|
8
|
+
|
9
|
+
REQUEST_URL = "https://api.capsolver.com"
|
10
|
+
VALID_STATUS_CODES = (200, 202, 400, 401, 405)
|
11
|
+
|
12
|
+
APP_ID = "6F895B2F-F454-44D1-8FE0-77ACAD3DBDC8"
|
13
|
+
|
14
|
+
|
15
|
+
# Connection retry generator
|
16
|
+
def attempts_generator(amount: int = 16) -> Generator:
|
17
|
+
"""
|
18
|
+
Function generates a generator of length equal to `amount`
|
19
|
+
|
20
|
+
Args:
|
21
|
+
amount: number of attempts generated
|
22
|
+
|
23
|
+
Yields:
|
24
|
+
int: The next number in the range of 1 to ``amount`` - 1.
|
25
|
+
|
26
|
+
Examples:
|
27
|
+
Examples should be written in doctest format, and should illustrate how
|
28
|
+
to use the function.
|
29
|
+
|
30
|
+
>>> print([i for i in attempts_generator(5)])
|
31
|
+
[1, 2, 3, 4]
|
32
|
+
|
33
|
+
Returns:
|
34
|
+
Attempt number
|
35
|
+
"""
|
36
|
+
yield from range(1, amount)
|