Skip to content

Commit 1365e65

Browse files
ad-mAdam Dobrawyclaude
authored
Modernize UX & developer experience: type hints, snake_case aliases, Proxy class, README fixes (#118)
- Add type hints to all public APIs in base.py, tasks.py, exceptions.py - Add Proxy frozen dataclass with parse_url() and to_kwargs() methods - Add snake_case aliases on AnticaptchaClient (create_task, get_balance, etc.) - Fix GeeTestTask bug: was inheriting wrong type "GeeTestTaskProxyless" - Add py.typed PEP 561 marker, Job and Proxy exports, __all__ list - Fix README: Python version claim, print() syntax, Proxy usage, error handling - Add requires-python to pyproject.toml for uv compatibility - Add tests for Proxy, snake_case aliases, and GeeTestTask type fix Co-authored-by: Adam Dobrawy <naczelnik@jawne.info.pl> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a4c14dd commit 1365e65

11 files changed

Lines changed: 283 additions & 97 deletions

File tree

README.rst

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ python-anticaptcha
1919
.. introduction-start
2020
2121
Client library for solve captchas with `Anticaptcha.com support`_.
22-
The library supports both Python 2.7 and Python 3.
22+
The library requires Python >= 3.9.
2323

2424
The library is cyclically and automatically tested for proper operation. We are constantly making the best efforts for its effective operation.
2525

@@ -67,9 +67,9 @@ Example snippet for Recaptcha:
6767
6868
client = AnticaptchaClient(api_key)
6969
task = NoCaptchaTaskProxylessTask(url, site_key)
70-
job = client.createTask(task)
70+
job = client.create_task(task)
7171
job.join()
72-
print job.get_solution_response()
72+
print(job.get_solution_response())
7373
7474
The full integration example is available in file ``examples/recaptcha.py``.
7575

@@ -89,9 +89,9 @@ measures for automated training and analysis. For provide that pass
8989
9090
client = AnticaptchaClient(api_key)
9191
task = NoCaptchaTaskProxylessTask(url, site_key, is_invisible=True)
92-
job = client.createTask(task)
92+
job = client.create_task(task)
9393
job.join()
94-
print job.get_solution_response()
94+
print(job.get_solution_response())
9595
9696
9797
Solve text captcha
@@ -107,9 +107,9 @@ Example snippet for text captcha:
107107
captcha_fp = open('examples/captcha_ms.jpeg', 'rb')
108108
client = AnticaptchaClient(api_key)
109109
task = ImageToTextTask(captcha_fp)
110-
job = client.createTask(task)
110+
job = client.create_task(task)
111111
job.join()
112-
print job.get_captcha_text()
112+
print(job.get_captcha_text())
113113
114114
Solve funcaptcha
115115
################
@@ -125,13 +125,13 @@ Example snippet for funcaptcha:
125125
api_key = '174faff8fbc769e94a5862391ecfd010'
126126
site_key = 'DE0B0BB7-1EE4-4D70-1853-31B835D4506B' # grab from site
127127
url = 'https://www.google.com/recaptcha/api2/demo'
128-
proxy = Proxy.parse_url("socks5://login:password@123.123.123.123")
128+
proxy = Proxy.parse_url("socks5://login:password@123.123.123.123:1080")
129129
130130
client = AnticaptchaClient(api_key)
131-
task = FunCaptchaTask(url, site_key, proxy=proxy, user_agent=user_agent)
132-
job = client.createTask(task)
131+
task = FunCaptchaTask(url, site_key, user_agent=UA, **proxy.to_kwargs())
132+
job = client.create_task(task)
133133
job.join()
134-
print job.get_token_response()
134+
print(job.get_token_response())
135135
136136
Report incorrect image
137137
######################
@@ -146,10 +146,10 @@ Example snippet for reporting an incorrect image task:
146146
captcha_fp = open('examples/captcha_ms.jpeg', 'rb')
147147
client = AnticaptchaClient(api_key)
148148
task = ImageToTextTask(captcha_fp)
149-
job = client.createTask(task)
149+
job = client.create_task(task)
150150
job.join()
151-
print job.get_captcha_text()
152-
job.report_incorrect()
151+
print(job.get_captcha_text())
152+
job.report_incorrect_image()
153153
154154
Setup proxy
155155
###########
@@ -181,20 +181,24 @@ We recommend entering IP-based access control for incoming addresses to proxy. I
181181
Error handling
182182
##############
183183

184-
In the event of an application error, the AnticaptchaException exception is thrown. To handle the exception, do the following:
184+
In the event of an application error, the ``AnticaptchaException`` exception is thrown. To handle the exception, do the following:
185185

186186
.. code:: python
187187
188-
from python_anticaptcha import AnticatpchaException, ImageToTextTask
188+
from python_anticaptcha import AnticaptchaException, ImageToTextTask
189189
190190
try:
191191
# any actions
192-
except AnticatpchaException as e:
192+
except AnticaptchaException as e:
193193
if e.error_code == 'ERROR_ZERO_BALANCE':
194194
notify_about_no_funds(e.error_id, e.error_code, e.error_description)
195195
else:
196196
raise
197197
198+
.. note::
199+
200+
The legacy misspelled ``AnticatpchaException`` alias is still available for backward compatibility.
201+
198202
.. usage-end
199203
200204
Versioning

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
[project]
2+
name = "python-anticaptcha"
3+
requires-python = ">=3.9"
4+
dynamic = ["version", "description", "readme", "dependencies", "optional-dependencies"]
5+
16
[tool.pytest.ini_options]
27
testpaths = ["tests"]
38
markers = [

python_anticaptcha/__init__.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from importlib.metadata import version, PackageNotFoundError
22

3-
from .base import AnticaptchaClient
3+
from .base import AnticaptchaClient, Job
4+
from .proxy import Proxy
45
from .tasks import (
56
NoCaptchaTaskProxylessTask,
67
RecaptchaV2TaskProxyless,
@@ -28,3 +29,27 @@
2829
except PackageNotFoundError:
2930
# package is not installed
3031
pass
32+
33+
__all__ = [
34+
"AnticaptchaClient",
35+
"Job",
36+
"Proxy",
37+
"NoCaptchaTaskProxylessTask",
38+
"RecaptchaV2TaskProxyless",
39+
"NoCaptchaTask",
40+
"RecaptchaV2Task",
41+
"FunCaptchaProxylessTask",
42+
"FunCaptchaTask",
43+
"ImageToTextTask",
44+
"RecaptchaV3TaskProxyless",
45+
"HCaptchaTaskProxyless",
46+
"HCaptchaTask",
47+
"RecaptchaV2EnterpriseTaskProxyless",
48+
"RecaptchaV2EnterpriseTask",
49+
"GeeTestTaskProxyless",
50+
"GeeTestTask",
51+
"AntiGateTaskProxyless",
52+
"AntiGateTask",
53+
"AnticaptchaException",
54+
"AnticatpchaException",
55+
]

python_anticaptcha/base.py

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
from __future__ import annotations
2+
13
import requests
24
import time
35
import json
46
import warnings
7+
from typing import Any
58

69
from urllib.parse import urljoin
710
from .exceptions import AnticaptchaException
@@ -15,49 +18,49 @@ class Job:
1518
task_id = None
1619
_last_result = None
1720

18-
def __init__(self, client, task_id):
21+
def __init__(self, client: AnticaptchaClient, task_id: int) -> None:
1922
self.client = client
2023
self.task_id = task_id
2124

22-
def _update(self):
25+
def _update(self) -> None:
2326
self._last_result = self.client.getTaskResult(self.task_id)
2427

25-
def check_is_ready(self):
28+
def check_is_ready(self) -> bool:
2629
self._update()
2730
return self._last_result["status"] == "ready"
2831

29-
def get_solution_response(self): # Recaptcha
32+
def get_solution_response(self) -> str: # Recaptcha
3033
return self._last_result["solution"]["gRecaptchaResponse"]
3134

32-
def get_solution(self):
35+
def get_solution(self) -> dict[str, Any]:
3336
return self._last_result["solution"]
3437

35-
def get_token_response(self): # Funcaptcha
38+
def get_token_response(self) -> str: # Funcaptcha
3639
return self._last_result["solution"]["token"]
3740

38-
def get_answers(self):
41+
def get_answers(self) -> dict[str, str]:
3942
return self._last_result["solution"]["answers"]
4043

41-
def get_captcha_text(self): # Image
44+
def get_captcha_text(self) -> str: # Image
4245
return self._last_result["solution"]["text"]
4346

44-
def get_cells_numbers(self):
47+
def get_cells_numbers(self) -> list[int]:
4548
return self._last_result["solution"]["cellNumbers"]
4649

47-
def report_incorrect(self):
50+
def report_incorrect(self) -> bool:
4851
warnings.warn(
4952
"report_incorrect is deprecated, use report_incorrect_image instead",
5053
DeprecationWarning,
5154
)
5255
return self.client.reportIncorrectImage()
5356

54-
def report_incorrect_image(self):
57+
def report_incorrect_image(self) -> bool:
5558
return self.client.reportIncorrectImage(self.task_id)
5659

57-
def report_incorrect_recaptcha(self):
60+
def report_incorrect_recaptcha(self) -> bool:
5861
return self.client.reportIncorrectRecaptcha(self.task_id)
5962

60-
def join(self, maximum_time=None):
63+
def join(self, maximum_time: int | None = None) -> None:
6164
elapsed_time = 0
6265
maximum_time = maximum_time or MAXIMUM_JOIN_TIME
6366
while not self.check_is_ready():
@@ -86,8 +89,8 @@ class AnticaptchaClient:
8689
response_timeout = 5
8790

8891
def __init__(
89-
self, client_key, language_pool="en", host="api.anti-captcha.com", use_ssl=True
90-
):
92+
self, client_key: str, language_pool: str = "en", host: str = "api.anti-captcha.com", use_ssl: bool = True,
93+
) -> None:
9194
self.client_key = client_key
9295
self.language_pool = language_pool
9396
self.base_url = "{proto}://{host}/".format(
@@ -96,14 +99,14 @@ def __init__(
9699
self.session = requests.Session()
97100

98101
@property
99-
def client_ip(self):
102+
def client_ip(self) -> str:
100103
if not hasattr(self, "_client_ip"):
101104
self._client_ip = self.session.get(
102105
"https://api.myip.com", timeout=self.response_timeout
103106
).json()["ip"]
104107
return self._client_ip
105108

106-
def _check_response(self, response):
109+
def _check_response(self, response: dict[str, Any]) -> None:
107110
if response.get("errorId", False) == 11:
108111
response[
109112
"errorDescription"
@@ -115,7 +118,7 @@ def _check_response(self, response):
115118
response["errorId"], response["errorCode"], response["errorDescription"]
116119
)
117120

118-
def createTask(self, task):
121+
def createTask(self, task: Any) -> Job:
119122
request = {
120123
"clientKey": self.client_key,
121124
"task": task.serialize(),
@@ -130,7 +133,7 @@ def createTask(self, task):
130133
self._check_response(response)
131134
return Job(self, response["taskId"])
132135

133-
def createTaskSmee(self, task, timeout=MAXIMUM_JOIN_TIME):
136+
def createTaskSmee(self, task: Any, timeout: int = MAXIMUM_JOIN_TIME) -> Job:
134137
"""
135138
Beta method to stream response from smee.io
136139
"""
@@ -172,15 +175,15 @@ def createTaskSmee(self, task, timeout=MAXIMUM_JOIN_TIME):
172175
job._last_result = payload["body"]
173176
return job
174177

175-
def getTaskResult(self, task_id):
178+
def getTaskResult(self, task_id: int) -> dict[str, Any]:
176179
request = {"clientKey": self.client_key, "taskId": task_id}
177180
response = self.session.post(
178181
urljoin(self.base_url, self.TASK_RESULT_URL), json=request
179182
).json()
180183
self._check_response(response)
181184
return response
182185

183-
def getBalance(self):
186+
def getBalance(self) -> float:
184187
request = {
185188
"clientKey": self.client_key,
186189
"softId": self.SOFT_ID,
@@ -191,26 +194,35 @@ def getBalance(self):
191194
self._check_response(response)
192195
return response["balance"]
193196

194-
def getAppStats(self, soft_id, mode):
197+
def getAppStats(self, soft_id: int, mode: str) -> dict[str, Any]:
195198
request = {"clientKey": self.client_key, "softId": soft_id, "mode": mode}
196199
response = self.session.post(
197200
urljoin(self.base_url, self.APP_STAT_URL), json=request
198201
).json()
199202
self._check_response(response)
200203
return response
201204

202-
def reportIncorrectImage(self, task_id):
205+
def reportIncorrectImage(self, task_id: int) -> bool:
203206
request = {"clientKey": self.client_key, "taskId": task_id}
204207
response = self.session.post(
205208
urljoin(self.base_url, self.REPORT_IMAGE_URL), json=request
206209
).json()
207210
self._check_response(response)
208211
return response.get("status", False) != False
209212

210-
def reportIncorrectRecaptcha(self, task_id):
213+
def reportIncorrectRecaptcha(self, task_id: int) -> bool:
211214
request = {"clientKey": self.client_key, "taskId": task_id}
212215
response = self.session.post(
213216
urljoin(self.base_url, self.REPORT_RECAPTCHA_URL), json=request
214217
).json()
215218
self._check_response(response)
216219
return response["status"] == "success"
220+
221+
# Snake_case aliases
222+
create_task = createTask
223+
create_task_smee = createTaskSmee
224+
get_task_result = getTaskResult
225+
get_balance = getBalance
226+
get_app_stats = getAppStats
227+
report_incorrect_image = reportIncorrectImage
228+
report_incorrect_recaptcha = reportIncorrectRecaptcha

python_anticaptcha/exceptions.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
from __future__ import annotations
2+
3+
14
class AnticaptchaException(Exception):
2-
def __init__(self, error_id, error_code, error_description, *args):
5+
def __init__(self, error_id: int | str | None, error_code: int | str, error_description: str, *args: object) -> None:
36
super().__init__(
47
"[{}:{}]{}".format(error_code, error_id, error_description)
58
)

python_anticaptcha/proxy.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
proxy_type: str
10+
proxy_address: str
11+
proxy_port: int
12+
proxy_login: str = ""
13+
proxy_password: str = ""
14+
15+
@classmethod
16+
def parse_url(cls, url: str) -> Proxy:
17+
parsed = urlparse(url)
18+
if not parsed.hostname or not parsed.port:
19+
raise ValueError(f"Invalid proxy URL: {url}")
20+
return cls(
21+
proxy_type=parsed.scheme,
22+
proxy_address=parsed.hostname,
23+
proxy_port=parsed.port,
24+
proxy_login=parsed.username or "",
25+
proxy_password=parsed.password or "",
26+
)
27+
28+
def to_kwargs(self) -> dict[str, str | int]:
29+
return {
30+
"proxy_type": self.proxy_type,
31+
"proxy_address": self.proxy_address,
32+
"proxy_port": self.proxy_port,
33+
"proxy_login": self.proxy_login,
34+
"proxy_password": self.proxy_password,
35+
}

python_anticaptcha/py.typed

Whitespace-only changes.

0 commit comments

Comments
 (0)