python-anticaptcha 0.0.4__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.
@@ -1,2 +1,86 @@
1
- from .base import AnticaptchaClient
2
- from .tasks import NoCaptchaTask, NoCaptchaTaskProxylessTask, ImageToTextTask
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
@@ -1,88 +1,2 @@
1
- import requests
2
- import time
3
-
4
- try:
5
- from urllib.parse import urljoin
6
- except ImportError:
7
- from urlparse 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 = 0 # TODO: Update to provide motivation for constant maintenance of the application.
51
- # This does not increase the cost of using the library.
52
- language_pool = "en"
53
-
54
- def __init__(self, client_key, language_pool="en", host="api.anti-captcha.com", use_ssl=True):
55
- self.client_key = client_key
56
- self.language_pool = language_pool
57
- self.base_url = "{proto}://{host}/".format(proto="https" if use_ssl else "http",
58
- host=host)
59
- self.session = requests.Session()
60
-
61
- def _check_response(self, response):
62
- if response.get('errorId', False):
63
- raise AnticatpchaException(response['errorId'],
64
- response['errorCode'],
65
- response['errorDescription'])
66
-
67
- def createTask(self, task):
68
- request = {"clientKey": self.client_key,
69
- "task": task.serialize(),
70
- "softId": self.SOFT_ID,
71
- "languagePool": self.language_pool,
72
- }
73
- response = self.session.post(urljoin(self.base_url, self.CREATE_TASK_URL), json=request).json()
74
- self._check_response(response)
75
- return Job(self, response['taskId'])
76
-
77
- def getTaskResult(self, task_id):
78
- request = {"clientKey": self.client_key,
79
- "taskId": task_id}
80
- response = self.session.post(urljoin(self.base_url, self.TASK_RESULT_URL), json=request).json()
81
- self._check_response(response)
82
- return response
83
-
84
- def getBalance(self):
85
- request = {"clientKey": self.client_key}
86
- response = self.session.post(urljoin(self.base_url, self.BALANCE_URL), json=request).json()
87
- self._check_response(response)
88
- return response['balance']
1
+ # Backward compatibility — canonical location is sync_client.py
2
+ from .sync_client import * # noqa: F401,F403
@@ -1,5 +1,57 @@
1
- class AnticatpchaException(Exception):
2
- def __init__(self, error_id, error_code, error_description, *args):
3
- super(AnticatpchaException, self).__init__(error_description)
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