python-anticaptcha 0.0.5__py3-none-any.whl → 2.0.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.
- python_anticaptcha/__init__.py +86 -2
- python_anticaptcha/async_client.py +364 -0
- python_anticaptcha/base.py +2 -87
- python_anticaptcha/exceptions.py +55 -3
- python_anticaptcha/proxy.py +67 -0
- python_anticaptcha/py.typed +0 -0
- python_anticaptcha/sync_client.py +402 -0
- python_anticaptcha/tasks.py +576 -51
- python_anticaptcha-2.0.0.dist-info/METADATA +277 -0
- python_anticaptcha-2.0.0.dist-info/RECORD +12 -0
- {python_anticaptcha-0.0.5.dist-info → python_anticaptcha-2.0.0.dist-info}/WHEEL +1 -2
- python_anticaptcha-2.0.0.dist-info/licenses/LICENSE.md +21 -0
- python_anticaptcha-0.0.5.dist-info/DESCRIPTION.rst +0 -73
- python_anticaptcha-0.0.5.dist-info/METADATA +0 -94
- python_anticaptcha-0.0.5.dist-info/RECORD +0 -10
- python_anticaptcha-0.0.5.dist-info/metadata.json +0 -1
- python_anticaptcha-0.0.5.dist-info/top_level.txt +0 -1
python_anticaptcha/__init__.py
CHANGED
|
@@ -1,2 +1,86 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"""Python client library for the `Anticaptcha.com <https://anti-captcha.com>`_ API.
|
|
2
|
+
|
|
3
|
+
Solve ReCAPTCHA v2/v3, hCaptcha, FunCaptcha, GeeTest, image-to-text, and
|
|
4
|
+
AntiGate tasks using human workers. Supports both synchronous (``requests``)
|
|
5
|
+
and asynchronous (``httpx``) usage.
|
|
6
|
+
|
|
7
|
+
Quick start::
|
|
8
|
+
|
|
9
|
+
from python_anticaptcha import AnticaptchaClient, NoCaptchaTaskProxylessTask
|
|
10
|
+
|
|
11
|
+
with AnticaptchaClient("my-api-key") as client:
|
|
12
|
+
task = NoCaptchaTaskProxylessTask(website_url, site_key)
|
|
13
|
+
job = client.create_task(task)
|
|
14
|
+
job.join()
|
|
15
|
+
print(job.get_solution_response())
|
|
16
|
+
|
|
17
|
+
For async usage, install with ``pip install python-anticaptcha[async]`` and use
|
|
18
|
+
:class:`AsyncAnticaptchaClient`.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import contextlib
|
|
22
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
23
|
+
|
|
24
|
+
from .exceptions import AnticaptchaException
|
|
25
|
+
from .proxy import Proxy
|
|
26
|
+
from .sync_client import AnticaptchaClient, Job
|
|
27
|
+
from .tasks import (
|
|
28
|
+
AntiGateTask,
|
|
29
|
+
AntiGateTaskProxyless,
|
|
30
|
+
FunCaptchaProxylessTask,
|
|
31
|
+
FunCaptchaTask,
|
|
32
|
+
GeeTestTask,
|
|
33
|
+
GeeTestTaskProxyless,
|
|
34
|
+
HCaptchaTask,
|
|
35
|
+
HCaptchaTaskProxyless,
|
|
36
|
+
ImageToTextTask,
|
|
37
|
+
NoCaptchaTask,
|
|
38
|
+
NoCaptchaTaskProxylessTask,
|
|
39
|
+
RecaptchaV2EnterpriseTask,
|
|
40
|
+
RecaptchaV2EnterpriseTaskProxyless,
|
|
41
|
+
RecaptchaV2Task,
|
|
42
|
+
RecaptchaV2TaskProxyless,
|
|
43
|
+
RecaptchaV3TaskProxyless,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
AnticatpchaException = AnticaptchaException
|
|
47
|
+
|
|
48
|
+
with contextlib.suppress(PackageNotFoundError):
|
|
49
|
+
__version__ = version(__name__)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def __getattr__(name: str) -> type:
|
|
53
|
+
if name in ("AsyncAnticaptchaClient", "AsyncJob"):
|
|
54
|
+
from .async_client import AsyncAnticaptchaClient, AsyncJob
|
|
55
|
+
|
|
56
|
+
globals()["AsyncAnticaptchaClient"] = AsyncAnticaptchaClient
|
|
57
|
+
globals()["AsyncJob"] = AsyncJob
|
|
58
|
+
return globals()[name]
|
|
59
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
__all__ = [
|
|
63
|
+
"AnticaptchaClient",
|
|
64
|
+
"Job",
|
|
65
|
+
"Proxy",
|
|
66
|
+
"NoCaptchaTaskProxylessTask",
|
|
67
|
+
"RecaptchaV2TaskProxyless",
|
|
68
|
+
"NoCaptchaTask",
|
|
69
|
+
"RecaptchaV2Task",
|
|
70
|
+
"FunCaptchaProxylessTask",
|
|
71
|
+
"FunCaptchaTask",
|
|
72
|
+
"ImageToTextTask",
|
|
73
|
+
"RecaptchaV3TaskProxyless",
|
|
74
|
+
"HCaptchaTaskProxyless",
|
|
75
|
+
"HCaptchaTask",
|
|
76
|
+
"RecaptchaV2EnterpriseTaskProxyless",
|
|
77
|
+
"RecaptchaV2EnterpriseTask",
|
|
78
|
+
"GeeTestTaskProxyless",
|
|
79
|
+
"GeeTestTask",
|
|
80
|
+
"AntiGateTaskProxyless",
|
|
81
|
+
"AntiGateTask",
|
|
82
|
+
"AnticaptchaException",
|
|
83
|
+
"AnticatpchaException",
|
|
84
|
+
"AsyncAnticaptchaClient",
|
|
85
|
+
"AsyncJob",
|
|
86
|
+
]
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
from typing import Any, Callable, Literal
|
|
7
|
+
from urllib.parse import urljoin
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import httpx # type: ignore[import-not-found]
|
|
11
|
+
except ImportError:
|
|
12
|
+
httpx = None # type: ignore[assignment]
|
|
13
|
+
|
|
14
|
+
from .exceptions import AnticaptchaException
|
|
15
|
+
from .tasks import BaseTask
|
|
16
|
+
|
|
17
|
+
SLEEP_EVERY_CHECK_FINISHED = 3
|
|
18
|
+
MAXIMUM_JOIN_TIME = 60 * 5
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AsyncJob:
|
|
22
|
+
"""An async handle to a submitted captcha-solving task.
|
|
23
|
+
|
|
24
|
+
Returned by :meth:`AsyncAnticaptchaClient.create_task`. Use :meth:`join`
|
|
25
|
+
to wait for completion, then call one of the ``get_*`` methods to
|
|
26
|
+
retrieve the solution.
|
|
27
|
+
|
|
28
|
+
Example::
|
|
29
|
+
|
|
30
|
+
job = await client.create_task(task)
|
|
31
|
+
await job.join()
|
|
32
|
+
print(job.get_solution_response()) # for ReCAPTCHA / hCaptcha
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
client: AsyncAnticaptchaClient
|
|
36
|
+
task_id: int
|
|
37
|
+
_last_result: dict[str, Any] | None = None
|
|
38
|
+
|
|
39
|
+
def __init__(self, client: AsyncAnticaptchaClient, task_id: int) -> None:
|
|
40
|
+
self.client = client
|
|
41
|
+
self.task_id = task_id
|
|
42
|
+
|
|
43
|
+
async def _update(self) -> None:
|
|
44
|
+
self._last_result = await self.client.getTaskResult(self.task_id)
|
|
45
|
+
|
|
46
|
+
async def check_is_ready(self) -> bool:
|
|
47
|
+
"""Poll the API once and return whether the task is complete.
|
|
48
|
+
|
|
49
|
+
:returns: ``True`` if the solution is ready, ``False`` otherwise.
|
|
50
|
+
"""
|
|
51
|
+
await self._update()
|
|
52
|
+
assert self._last_result is not None
|
|
53
|
+
return self._last_result["status"] == "ready"
|
|
54
|
+
|
|
55
|
+
def get_solution_response(self) -> str:
|
|
56
|
+
"""Return the ``gRecaptchaResponse`` token.
|
|
57
|
+
|
|
58
|
+
Use after solving ReCAPTCHA v2, ReCAPTCHA v3, or hCaptcha tasks.
|
|
59
|
+
Call this only after :meth:`join` has returned.
|
|
60
|
+
"""
|
|
61
|
+
assert self._last_result is not None
|
|
62
|
+
return self._last_result["solution"]["gRecaptchaResponse"]
|
|
63
|
+
|
|
64
|
+
def get_solution(self) -> dict[str, Any]:
|
|
65
|
+
"""Return the full solution dictionary from the API response.
|
|
66
|
+
|
|
67
|
+
Useful for task types where the solution has multiple fields
|
|
68
|
+
(e.g. GeeTest returns ``challenge``, ``validate``, ``seccode``).
|
|
69
|
+
Call this only after :meth:`join` has returned.
|
|
70
|
+
"""
|
|
71
|
+
assert self._last_result is not None
|
|
72
|
+
return self._last_result["solution"]
|
|
73
|
+
|
|
74
|
+
def get_token_response(self) -> str:
|
|
75
|
+
"""Return the ``token`` string from the solution.
|
|
76
|
+
|
|
77
|
+
Use after solving FunCaptcha tasks.
|
|
78
|
+
Call this only after :meth:`join` has returned.
|
|
79
|
+
"""
|
|
80
|
+
assert self._last_result is not None
|
|
81
|
+
return self._last_result["solution"]["token"]
|
|
82
|
+
|
|
83
|
+
def get_answers(self) -> dict[str, str]:
|
|
84
|
+
"""Return the ``answers`` dictionary from the solution.
|
|
85
|
+
|
|
86
|
+
Use after solving AntiGate tasks.
|
|
87
|
+
Call this only after :meth:`join` has returned.
|
|
88
|
+
"""
|
|
89
|
+
assert self._last_result is not None
|
|
90
|
+
return self._last_result["solution"]["answers"]
|
|
91
|
+
|
|
92
|
+
def get_captcha_text(self) -> str:
|
|
93
|
+
"""Return the recognized text from an image captcha.
|
|
94
|
+
|
|
95
|
+
Use after solving :class:`ImageToTextTask` tasks.
|
|
96
|
+
Call this only after :meth:`join` has returned.
|
|
97
|
+
"""
|
|
98
|
+
assert self._last_result is not None
|
|
99
|
+
return self._last_result["solution"]["text"]
|
|
100
|
+
|
|
101
|
+
def get_cells_numbers(self) -> list[int]:
|
|
102
|
+
"""Return the list of selected cell numbers from a grid captcha.
|
|
103
|
+
|
|
104
|
+
Call this only after :meth:`join` has returned.
|
|
105
|
+
"""
|
|
106
|
+
assert self._last_result is not None
|
|
107
|
+
return self._last_result["solution"]["cellNumbers"]
|
|
108
|
+
|
|
109
|
+
async def report_incorrect_image(self) -> bool:
|
|
110
|
+
"""Report that an image captcha was solved incorrectly.
|
|
111
|
+
|
|
112
|
+
:returns: ``True`` if the report was accepted.
|
|
113
|
+
"""
|
|
114
|
+
return await self.client.reportIncorrectImage(self.task_id)
|
|
115
|
+
|
|
116
|
+
async def report_incorrect_recaptcha(self) -> bool:
|
|
117
|
+
"""Report that a ReCAPTCHA was solved incorrectly.
|
|
118
|
+
|
|
119
|
+
:returns: ``True`` if the report was accepted.
|
|
120
|
+
"""
|
|
121
|
+
return await self.client.reportIncorrectRecaptcha(self.task_id)
|
|
122
|
+
|
|
123
|
+
def __repr__(self) -> str:
|
|
124
|
+
status = self._last_result.get("status") if self._last_result else None
|
|
125
|
+
if status:
|
|
126
|
+
return f"<AsyncJob task_id={self.task_id} status={status!r}>"
|
|
127
|
+
return f"<AsyncJob task_id={self.task_id}>"
|
|
128
|
+
|
|
129
|
+
async def join(
|
|
130
|
+
self,
|
|
131
|
+
maximum_time: int | None = None,
|
|
132
|
+
on_check: Callable[[int, str | None], None] | None = None,
|
|
133
|
+
backoff: bool = False,
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Poll for task completion, sleeping asynchronously until ready or timeout.
|
|
136
|
+
|
|
137
|
+
:param maximum_time: Maximum seconds to wait (default: ``MAXIMUM_JOIN_TIME``).
|
|
138
|
+
:param on_check: Optional callback invoked after each poll with
|
|
139
|
+
``(elapsed_time, status)`` where *elapsed_time* is the total seconds
|
|
140
|
+
waited so far and *status* is the last task status string
|
|
141
|
+
(e.g. ``"processing"``).
|
|
142
|
+
:param backoff: When ``True``, use exponential backoff for polling
|
|
143
|
+
intervals starting at 1 second and doubling up to a 10-second cap.
|
|
144
|
+
Default ``False`` preserves the fixed 3-second interval.
|
|
145
|
+
:raises AnticaptchaException: If *maximum_time* is exceeded.
|
|
146
|
+
"""
|
|
147
|
+
elapsed_time = 0
|
|
148
|
+
maximum_time = maximum_time or MAXIMUM_JOIN_TIME
|
|
149
|
+
sleep_time = 1 if backoff else SLEEP_EVERY_CHECK_FINISHED
|
|
150
|
+
while not await self.check_is_ready():
|
|
151
|
+
await asyncio.sleep(sleep_time)
|
|
152
|
+
elapsed_time += sleep_time
|
|
153
|
+
if backoff:
|
|
154
|
+
sleep_time = min(sleep_time * 2, 10)
|
|
155
|
+
if on_check is not None and self._last_result is not None:
|
|
156
|
+
on_check(elapsed_time, self._last_result.get("status"))
|
|
157
|
+
if elapsed_time > maximum_time:
|
|
158
|
+
raise AnticaptchaException(
|
|
159
|
+
None,
|
|
160
|
+
250,
|
|
161
|
+
f"The execution time exceeded a maximum time of {maximum_time} seconds."
|
|
162
|
+
f" It takes {elapsed_time} seconds.",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class AsyncAnticaptchaClient:
|
|
167
|
+
"""Asynchronous client for the Anticaptcha.com API.
|
|
168
|
+
|
|
169
|
+
Mirrors :class:`AnticaptchaClient` but all network methods are coroutines.
|
|
170
|
+
Requires the ``httpx`` package — install with
|
|
171
|
+
``pip install python-anticaptcha[async]``.
|
|
172
|
+
|
|
173
|
+
Can be used as an async context manager::
|
|
174
|
+
|
|
175
|
+
async with AsyncAnticaptchaClient("my-api-key") as client:
|
|
176
|
+
job = await client.create_task(task)
|
|
177
|
+
await job.join()
|
|
178
|
+
|
|
179
|
+
:param client_key: Your Anticaptcha API key. If omitted, the
|
|
180
|
+
``ANTICAPTCHA_API_KEY`` environment variable is used.
|
|
181
|
+
:param language_pool: Language pool for workers — ``"en"`` (default)
|
|
182
|
+
or ``"rn"`` (Russian).
|
|
183
|
+
:param host: API hostname (default: ``"api.anti-captcha.com"``).
|
|
184
|
+
:param use_ssl: Use HTTPS (default: ``True``).
|
|
185
|
+
:raises ImportError: If ``httpx`` is not installed.
|
|
186
|
+
:raises AnticaptchaException: If no API key is provided.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
client_key = None
|
|
190
|
+
CREATE_TASK_URL = "/createTask"
|
|
191
|
+
TASK_RESULT_URL = "/getTaskResult"
|
|
192
|
+
BALANCE_URL = "/getBalance"
|
|
193
|
+
REPORT_IMAGE_URL = "/reportIncorrectImageCaptcha"
|
|
194
|
+
REPORT_RECAPTCHA_URL = "/reportIncorrectRecaptcha"
|
|
195
|
+
APP_STAT_URL = "/getAppStats"
|
|
196
|
+
SOFT_ID = 847
|
|
197
|
+
language_pool = "en"
|
|
198
|
+
response_timeout = 5
|
|
199
|
+
|
|
200
|
+
def __init__(
|
|
201
|
+
self,
|
|
202
|
+
client_key: str | None = None,
|
|
203
|
+
language_pool: str = "en",
|
|
204
|
+
host: str = "api.anti-captcha.com",
|
|
205
|
+
use_ssl: bool = True,
|
|
206
|
+
) -> None:
|
|
207
|
+
if httpx is None:
|
|
208
|
+
raise ImportError(
|
|
209
|
+
"httpx is required for async support. Install it with: pip install python-anticaptcha[async]"
|
|
210
|
+
)
|
|
211
|
+
self.client_key = client_key or os.environ.get("ANTICAPTCHA_API_KEY")
|
|
212
|
+
if not self.client_key:
|
|
213
|
+
raise AnticaptchaException(
|
|
214
|
+
None,
|
|
215
|
+
"CONFIG_ERROR",
|
|
216
|
+
"API key required. Pass client_key or set ANTICAPTCHA_API_KEY env var.",
|
|
217
|
+
)
|
|
218
|
+
self.language_pool = language_pool
|
|
219
|
+
self.base_url = "{proto}://{host}/".format(proto="https" if use_ssl else "http", host=host)
|
|
220
|
+
self.session = httpx.AsyncClient()
|
|
221
|
+
|
|
222
|
+
async def __aenter__(self) -> AsyncAnticaptchaClient:
|
|
223
|
+
return self
|
|
224
|
+
|
|
225
|
+
async def __aexit__(
|
|
226
|
+
self,
|
|
227
|
+
exc_type: type[BaseException] | None,
|
|
228
|
+
exc_val: BaseException | None,
|
|
229
|
+
exc_tb: TracebackType | None,
|
|
230
|
+
) -> Literal[False]:
|
|
231
|
+
await self.session.aclose()
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
async def close(self) -> None:
|
|
235
|
+
"""Close the underlying HTTP session.
|
|
236
|
+
|
|
237
|
+
Called automatically when using the client as an async context manager.
|
|
238
|
+
"""
|
|
239
|
+
await self.session.aclose()
|
|
240
|
+
|
|
241
|
+
def __repr__(self) -> str:
|
|
242
|
+
from urllib.parse import urlparse
|
|
243
|
+
|
|
244
|
+
host = urlparse(self.base_url).hostname or self.base_url
|
|
245
|
+
return f"<AsyncAnticaptchaClient host={host!r}>"
|
|
246
|
+
|
|
247
|
+
async def _get_client_ip(self) -> str:
|
|
248
|
+
if not hasattr(self, "_client_ip"):
|
|
249
|
+
response = await self.session.get("https://api.myip.com", timeout=self.response_timeout)
|
|
250
|
+
self._client_ip = response.json()["ip"]
|
|
251
|
+
return self._client_ip
|
|
252
|
+
|
|
253
|
+
async def _check_response(self, response: dict[str, Any]) -> None:
|
|
254
|
+
if response.get("errorId", False) == 11:
|
|
255
|
+
ip = await self._get_client_ip()
|
|
256
|
+
response["errorDescription"] = "{} Your missing IP address is probably {}.".format(
|
|
257
|
+
response["errorDescription"], ip
|
|
258
|
+
)
|
|
259
|
+
if response.get("errorId", False):
|
|
260
|
+
raise AnticaptchaException(response["errorId"], response["errorCode"], response["errorDescription"])
|
|
261
|
+
|
|
262
|
+
async def createTask(self, task: BaseTask) -> AsyncJob:
|
|
263
|
+
"""Submit a captcha task and return an :class:`AsyncJob` handle.
|
|
264
|
+
|
|
265
|
+
:param task: A task instance (e.g. :class:`NoCaptchaTaskProxylessTask`).
|
|
266
|
+
:returns: An :class:`AsyncJob` that can be polled with :meth:`AsyncJob.join`.
|
|
267
|
+
:raises AnticaptchaException: If the API returns an error.
|
|
268
|
+
"""
|
|
269
|
+
request = {
|
|
270
|
+
"clientKey": self.client_key,
|
|
271
|
+
"task": task.serialize(),
|
|
272
|
+
"softId": self.SOFT_ID,
|
|
273
|
+
"languagePool": self.language_pool,
|
|
274
|
+
}
|
|
275
|
+
response = (
|
|
276
|
+
await self.session.post(
|
|
277
|
+
urljoin(self.base_url, self.CREATE_TASK_URL),
|
|
278
|
+
json=request,
|
|
279
|
+
timeout=self.response_timeout,
|
|
280
|
+
)
|
|
281
|
+
).json()
|
|
282
|
+
await self._check_response(response)
|
|
283
|
+
return AsyncJob(self, response["taskId"])
|
|
284
|
+
|
|
285
|
+
async def getTaskResult(self, task_id: int) -> dict[str, Any]:
|
|
286
|
+
"""Fetch the current result/status of a task.
|
|
287
|
+
|
|
288
|
+
:param task_id: The task ID returned when the task was created.
|
|
289
|
+
:returns: Raw API response dictionary with ``status`` and ``solution`` keys.
|
|
290
|
+
:raises AnticaptchaException: If the API returns an error.
|
|
291
|
+
"""
|
|
292
|
+
request = {"clientKey": self.client_key, "taskId": task_id}
|
|
293
|
+
response = (await self.session.post(urljoin(self.base_url, self.TASK_RESULT_URL), json=request)).json()
|
|
294
|
+
await self._check_response(response)
|
|
295
|
+
return response
|
|
296
|
+
|
|
297
|
+
async def getBalance(self) -> float:
|
|
298
|
+
"""Return the current account balance in USD.
|
|
299
|
+
|
|
300
|
+
:returns: Account balance as a float (e.g. ``3.50``).
|
|
301
|
+
:raises AnticaptchaException: If the API returns an error.
|
|
302
|
+
"""
|
|
303
|
+
request = {
|
|
304
|
+
"clientKey": self.client_key,
|
|
305
|
+
"softId": self.SOFT_ID,
|
|
306
|
+
}
|
|
307
|
+
response = (await self.session.post(urljoin(self.base_url, self.BALANCE_URL), json=request)).json()
|
|
308
|
+
await self._check_response(response)
|
|
309
|
+
return response["balance"]
|
|
310
|
+
|
|
311
|
+
async def getAppStats(self, soft_id: int, mode: str) -> dict[str, Any]:
|
|
312
|
+
"""Retrieve application statistics.
|
|
313
|
+
|
|
314
|
+
:param soft_id: Application ID.
|
|
315
|
+
:param mode: Statistics mode (e.g. ``"errors"``, ``"views"``, ``"downloads"``).
|
|
316
|
+
:returns: Raw API response dictionary with statistics data.
|
|
317
|
+
:raises AnticaptchaException: If the API returns an error.
|
|
318
|
+
"""
|
|
319
|
+
request = {"clientKey": self.client_key, "softId": soft_id, "mode": mode}
|
|
320
|
+
response = (await self.session.post(urljoin(self.base_url, self.APP_STAT_URL), json=request)).json()
|
|
321
|
+
await self._check_response(response)
|
|
322
|
+
return response
|
|
323
|
+
|
|
324
|
+
async def reportIncorrectImage(self, task_id: int) -> bool:
|
|
325
|
+
"""Report that an image captcha was solved incorrectly.
|
|
326
|
+
|
|
327
|
+
Use this to get a refund and improve solver accuracy.
|
|
328
|
+
|
|
329
|
+
:param task_id: The task ID of the incorrectly solved task.
|
|
330
|
+
:returns: ``True`` if the report was accepted.
|
|
331
|
+
:raises AnticaptchaException: If the API returns an error.
|
|
332
|
+
"""
|
|
333
|
+
request = {"clientKey": self.client_key, "taskId": task_id}
|
|
334
|
+
response = (await self.session.post(urljoin(self.base_url, self.REPORT_IMAGE_URL), json=request)).json()
|
|
335
|
+
await self._check_response(response)
|
|
336
|
+
return bool(response.get("status", False))
|
|
337
|
+
|
|
338
|
+
async def reportIncorrectRecaptcha(self, task_id: int) -> bool:
|
|
339
|
+
"""Report that a ReCAPTCHA was solved incorrectly.
|
|
340
|
+
|
|
341
|
+
Use this to get a refund and improve solver accuracy.
|
|
342
|
+
|
|
343
|
+
:param task_id: The task ID of the incorrectly solved task.
|
|
344
|
+
:returns: ``True`` if the report was accepted.
|
|
345
|
+
:raises AnticaptchaException: If the API returns an error.
|
|
346
|
+
"""
|
|
347
|
+
request = {"clientKey": self.client_key, "taskId": task_id}
|
|
348
|
+
response = (await self.session.post(urljoin(self.base_url, self.REPORT_RECAPTCHA_URL), json=request)).json()
|
|
349
|
+
await self._check_response(response)
|
|
350
|
+
return response["status"] == "success"
|
|
351
|
+
|
|
352
|
+
# Snake_case aliases
|
|
353
|
+
#: Alias for :meth:`createTask`.
|
|
354
|
+
create_task = createTask
|
|
355
|
+
#: Alias for :meth:`getTaskResult`.
|
|
356
|
+
get_task_result = getTaskResult
|
|
357
|
+
#: Alias for :meth:`getBalance`.
|
|
358
|
+
get_balance = getBalance
|
|
359
|
+
#: Alias for :meth:`getAppStats`.
|
|
360
|
+
get_app_stats = getAppStats
|
|
361
|
+
#: Alias for :meth:`reportIncorrectImage`.
|
|
362
|
+
report_incorrect_image = reportIncorrectImage
|
|
363
|
+
#: Alias for :meth:`reportIncorrectRecaptcha`.
|
|
364
|
+
report_incorrect_recaptcha = reportIncorrectRecaptcha
|
python_anticaptcha/base.py
CHANGED
|
@@ -1,87 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
try:
|
|
5
|
-
from urllib.parse import urljoin
|
|
6
|
-
except ImportError:
|
|
7
|
-
from urllib.parse import urljoin
|
|
8
|
-
from .exceptions import AnticatpchaException
|
|
9
|
-
|
|
10
|
-
SLEEP_EVERY_CHECK_FINISHED = 3
|
|
11
|
-
MAXIMUM_JOIN_TIME = 60
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class Job(object):
|
|
15
|
-
client = None
|
|
16
|
-
task_id = None
|
|
17
|
-
_last_result = None
|
|
18
|
-
|
|
19
|
-
def __init__(self, client, task_id):
|
|
20
|
-
self.client = client
|
|
21
|
-
self.task_id = task_id
|
|
22
|
-
|
|
23
|
-
def _update(self):
|
|
24
|
-
self._last_result = self.client.getTaskResult(self.task_id)
|
|
25
|
-
|
|
26
|
-
def check_is_ready(self):
|
|
27
|
-
self._update()
|
|
28
|
-
return self._last_result['status'] == 'ready'
|
|
29
|
-
|
|
30
|
-
def get_solution_response(self): # TODO: Support different captcha solutions
|
|
31
|
-
return self._last_result['solution']['gRecaptchaResponse']
|
|
32
|
-
|
|
33
|
-
def get_captcha_text(self):
|
|
34
|
-
return self._last_result['solution']['text']
|
|
35
|
-
|
|
36
|
-
def join(self, maximum_time=None):
|
|
37
|
-
elapsed_time = 0
|
|
38
|
-
while not self.check_is_ready():
|
|
39
|
-
time.sleep(SLEEP_EVERY_CHECK_FINISHED)
|
|
40
|
-
elapsed_time += SLEEP_EVERY_CHECK_FINISHED
|
|
41
|
-
if elapsed_time is not None and elapsed_time > MAXIMUM_JOIN_TIME:
|
|
42
|
-
raise AnticatpchaException(None, 250, "The maximum execution time of the task has been reached.")
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class AnticaptchaClient(object):
|
|
46
|
-
client_key = None
|
|
47
|
-
CREATE_TASK_URL = "/createTask"
|
|
48
|
-
TASK_RESULT_URL = "/getTaskResult"
|
|
49
|
-
BALANCE_URL = "/getBalance"
|
|
50
|
-
SOFT_ID = 847
|
|
51
|
-
language_pool = "en"
|
|
52
|
-
|
|
53
|
-
def __init__(self, client_key, language_pool="en", host="api.anti-captcha.com", use_ssl=True):
|
|
54
|
-
self.client_key = client_key
|
|
55
|
-
self.language_pool = language_pool
|
|
56
|
-
self.base_url = "{proto}://{host}/".format(proto="https" if use_ssl else "http",
|
|
57
|
-
host=host)
|
|
58
|
-
self.session = requests.Session()
|
|
59
|
-
|
|
60
|
-
def _check_response(self, response):
|
|
61
|
-
if response.get('errorId', False):
|
|
62
|
-
raise AnticatpchaException(response['errorId'],
|
|
63
|
-
response['errorCode'],
|
|
64
|
-
response['errorDescription'])
|
|
65
|
-
|
|
66
|
-
def createTask(self, task):
|
|
67
|
-
request = {"clientKey": self.client_key,
|
|
68
|
-
"task": task.serialize(),
|
|
69
|
-
"softId": self.SOFT_ID,
|
|
70
|
-
"languagePool": self.language_pool,
|
|
71
|
-
}
|
|
72
|
-
response = self.session.post(urljoin(self.base_url, self.CREATE_TASK_URL), json=request).json()
|
|
73
|
-
self._check_response(response)
|
|
74
|
-
return Job(self, response['taskId'])
|
|
75
|
-
|
|
76
|
-
def getTaskResult(self, task_id):
|
|
77
|
-
request = {"clientKey": self.client_key,
|
|
78
|
-
"taskId": task_id}
|
|
79
|
-
response = self.session.post(urljoin(self.base_url, self.TASK_RESULT_URL), json=request).json()
|
|
80
|
-
self._check_response(response)
|
|
81
|
-
return response
|
|
82
|
-
|
|
83
|
-
def getBalance(self):
|
|
84
|
-
request = {"clientKey": self.client_key}
|
|
85
|
-
response = self.session.post(urljoin(self.base_url, self.BALANCE_URL), json=request).json()
|
|
86
|
-
self._check_response(response)
|
|
87
|
-
return response['balance']
|
|
1
|
+
# Backward compatibility — canonical location is sync_client.py
|
|
2
|
+
from .sync_client import * # noqa: F401,F403
|
python_anticaptcha/exceptions.py
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AnticaptchaException(Exception):
|
|
5
|
+
"""Base exception for all Anticaptcha API errors.
|
|
6
|
+
|
|
7
|
+
Raised when the API returns a non-zero ``errorId``. Inspect
|
|
8
|
+
:attr:`error_code` to determine the cause::
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
job = client.create_task(task)
|
|
12
|
+
except AnticaptchaException as e:
|
|
13
|
+
if e.error_code == "ERROR_ZERO_BALANCE":
|
|
14
|
+
print("Please top up your balance")
|
|
15
|
+
else:
|
|
16
|
+
raise
|
|
17
|
+
|
|
18
|
+
:param error_id: Numeric error ID from the API (or a local identifier).
|
|
19
|
+
:param error_code: Error code string (e.g. ``"ERROR_ZERO_BALANCE"``).
|
|
20
|
+
:param error_description: Human-readable error description.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
error_id: int | str | None,
|
|
26
|
+
error_code: int | str,
|
|
27
|
+
error_description: str,
|
|
28
|
+
*args: object,
|
|
29
|
+
) -> None:
|
|
30
|
+
super().__init__(f"[{error_code}:{error_id}]{error_description}")
|
|
31
|
+
self.error_description = error_description
|
|
4
32
|
self.error_id = error_id
|
|
5
33
|
self.error_code = error_code
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
AnticatpchaException = AnticaptchaException
|
|
37
|
+
"""Backward-compatible alias (legacy misspelling)."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class InvalidWidthException(AnticaptchaException):
|
|
41
|
+
"""Raised when an invalid grid width is specified."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, width: int) -> None:
|
|
44
|
+
self.width = width
|
|
45
|
+
msg = f"Invalid width ({self.width}). Can be one of these: 100, 50, 33, 25."
|
|
46
|
+
super().__init__("AC-1", 1, msg)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class MissingNameException(AnticaptchaException):
|
|
50
|
+
"""Raised when a required ``name`` parameter is missing during serialization."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, cls: type) -> None:
|
|
53
|
+
self.cls = cls
|
|
54
|
+
msg = 'Missing name data in {0}. Provide {0}.__init__(name="X") or {0}.serialize(name="X")'.format(
|
|
55
|
+
str(self.cls)
|
|
56
|
+
)
|
|
57
|
+
super().__init__("AC-2", 2, msg)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from urllib.parse import urlparse
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class Proxy:
|
|
9
|
+
"""Immutable representation of a proxy server.
|
|
10
|
+
|
|
11
|
+
Use :meth:`parse_url` to build from a URL string, then pass the proxy
|
|
12
|
+
parameters to a proxy-enabled task class with :meth:`to_kwargs`::
|
|
13
|
+
|
|
14
|
+
proxy = Proxy.parse_url("socks5://user:pass@host:1080")
|
|
15
|
+
task = NoCaptchaTask(url, key, user_agent=UA, **proxy.to_kwargs())
|
|
16
|
+
|
|
17
|
+
:param proxy_type: Protocol — ``"http"``, ``"socks4"``, or ``"socks5"``.
|
|
18
|
+
:param proxy_address: Hostname or IP address.
|
|
19
|
+
:param proxy_port: Port number.
|
|
20
|
+
:param proxy_login: Username for authentication (default: ``""``).
|
|
21
|
+
:param proxy_password: Password for authentication (default: ``""``).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
proxy_type: str
|
|
25
|
+
proxy_address: str
|
|
26
|
+
proxy_port: int
|
|
27
|
+
proxy_login: str = ""
|
|
28
|
+
proxy_password: str = ""
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def parse_url(cls, url: str) -> Proxy:
|
|
32
|
+
"""Create a :class:`Proxy` from a URL string.
|
|
33
|
+
|
|
34
|
+
:param url: Proxy URL, e.g. ``"socks5://user:pass@host:1080"``
|
|
35
|
+
or ``"http://host:8080"``.
|
|
36
|
+
:returns: A new :class:`Proxy` instance.
|
|
37
|
+
:raises ValueError: If the URL is missing a hostname or port.
|
|
38
|
+
"""
|
|
39
|
+
parsed = urlparse(url)
|
|
40
|
+
if not parsed.hostname or not parsed.port:
|
|
41
|
+
raise ValueError(f"Invalid proxy URL: {url}")
|
|
42
|
+
return cls(
|
|
43
|
+
proxy_type=parsed.scheme,
|
|
44
|
+
proxy_address=parsed.hostname,
|
|
45
|
+
proxy_port=parsed.port,
|
|
46
|
+
proxy_login=parsed.username or "",
|
|
47
|
+
proxy_password=parsed.password or "",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def to_kwargs(self) -> dict[str, str | int]:
|
|
51
|
+
"""Convert to a keyword-arguments dictionary for task constructors.
|
|
52
|
+
|
|
53
|
+
The returned dictionary can be unpacked directly into any
|
|
54
|
+
proxy-enabled task class::
|
|
55
|
+
|
|
56
|
+
task = NoCaptchaTask(url, key, user_agent=UA, **proxy.to_kwargs())
|
|
57
|
+
|
|
58
|
+
:returns: Dictionary with ``proxy_type``, ``proxy_address``,
|
|
59
|
+
``proxy_port``, ``proxy_login``, and ``proxy_password`` keys.
|
|
60
|
+
"""
|
|
61
|
+
return {
|
|
62
|
+
"proxy_type": self.proxy_type,
|
|
63
|
+
"proxy_address": self.proxy_address,
|
|
64
|
+
"proxy_port": self.proxy_port,
|
|
65
|
+
"proxy_login": self.proxy_login,
|
|
66
|
+
"proxy_password": self.proxy_password,
|
|
67
|
+
}
|
|
File without changes
|