Skip to content

Commit ac6f62d

Browse files
ad-mAdam Dobrawyclaude
authored
Drop Python 2 support, require Python >= 3.9, modernize codebase (#115)
- Remove `six` dependency and `compat.py` (use stdlib `urllib.parse`) - Remove `importlib_metadata` fallback (stdlib since 3.8) - Use `super()` without args (Python 3 style) across all classes - Remove `(object)` from class definitions - Fix `RecaptchaV2EnterpriseTask` inheritance (was missing proxyless base) - Fix `ImageToTextTask.serialize` sending None values instead of omitting - Remove dead `CustomCaptchaTask` reference in `createTaskSmee` - Update classifiers to Python 3.9-3.14 - Add `python_requires=">=3.9"` and `long_description_content_type` - Remove `universal = 1` from bdist_wheel config - Update CI: actions/checkout@v4, setup-python@v5, Python 3.9-3.14 matrix - Fix duplicate `make docs` step in pythonpackage.yml - Update e2e.yml to Python 3.12 and nanasess/setup-chromedriver@v2 - Update tox.ini environments and commands Co-authored-by: Adam Dobrawy <naczelnik@jawne.info.pl> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b502215 commit ac6f62d

10 files changed

Lines changed: 82 additions & 105 deletions

File tree

.github/workflows/e2e.yml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,20 @@ jobs:
1111
runs-on: ubuntu-latest
1212
timeout-minutes: 30
1313
steps:
14-
- uses: actions/checkout@v1
14+
- uses: actions/checkout@v4
1515

16-
- name: Set up Python 3.7
17-
uses: actions/setup-python@v1
16+
- name: Set up Python 3.12
17+
uses: actions/setup-python@v5
1818
with:
19-
python-version: "3.7"
19+
python-version: "3.12"
20+
21+
- name: Install setup dependencies
22+
run: pip install setuptools_scm wheel
2023

2124
- name: Build distribution
2225
run: make install
23-
24-
- uses: nanasess/setup-chromedriver@v1
26+
27+
- uses: nanasess/setup-chromedriver@v2
2528

2629
- name: Run integration tests
2730
run: make test

.github/workflows/pythonpackage.yml

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,18 @@ jobs:
1111
fail-fast: false
1212
matrix:
1313
python-version:
14-
- '2.7'
15-
- '3.5'
16-
- '3.6'
17-
- '3.7'
18-
- '3.8'
1914
- '3.9'
2015
- '3.10'
16+
- '3.11'
17+
- '3.12'
18+
- '3.13'
19+
- '3.14'
2120

2221
steps:
23-
- uses: actions/checkout@v1
22+
- uses: actions/checkout@v4
2423

2524
- name: Set up Python ${{ matrix.python-version }}
26-
uses: actions/setup-python@v3
25+
uses: actions/setup-python@v5
2726
with:
2827
python-version: ${{ matrix.python-version }}
2928

@@ -36,8 +35,5 @@ jobs:
3635
- name: Build docs
3736
run: make docs
3837

39-
- name: Build docs
40-
run: make docs
41-
4238
- name: Test with pytest
43-
run: make test
39+
run: make test

python_anticaptcha/__init__.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1+
from importlib.metadata import version, PackageNotFoundError
2+
13
from .base import AnticaptchaClient
2-
try:
3-
from importlib.metadata import version, PackageNotFoundError
4-
except ImportError:
5-
# Python < 3.8 fallback
6-
from importlib_metadata import version, PackageNotFoundError
74
from .tasks import (
85
NoCaptchaTaskProxylessTask,
96
RecaptchaV2TaskProxyless,
@@ -20,7 +17,7 @@
2017
GeeTestTaskProxyless,
2118
GeeTestTask,
2219
AntiGateTaskProxyless,
23-
AntiGateTask
20+
AntiGateTask,
2421
)
2522
from .exceptions import AnticaptchaException
2623

python_anticaptcha/base.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@
33
import json
44
import warnings
55

6-
from .compat import split
7-
from six.moves.urllib_parse import urljoin
6+
from urllib.parse import urljoin
87
from .exceptions import AnticaptchaException
98

109
SLEEP_EVERY_CHECK_FINISHED = 3
1110
MAXIMUM_JOIN_TIME = 60 * 5
1211

1312

14-
class Job(object):
13+
class Job:
1514
client = None
1615
task_id = None
1716
_last_result = None
@@ -74,7 +73,7 @@ def join(self, maximum_time=None):
7473
)
7574

7675

77-
class AnticaptchaClient(object):
76+
class AnticaptchaClient:
7877
client_key = None
7978
CREATE_TASK_URL = "/createTask"
8079
TASK_RESULT_URL = "/getTaskResult"
@@ -163,14 +162,12 @@ def createTaskSmee(self, task, timeout=MAXIMUM_JOIN_TIME):
163162
content = line.decode("utf-8")
164163
if '"host":"smee.io"' not in content:
165164
continue
166-
payload = json.loads(split(content, ":", 1)[1].strip())
165+
payload = json.loads(content.split(":", maxsplit=1)[1].strip())
167166
if "taskId" not in payload["body"] or str(payload["body"]["taskId"]) != str(
168167
response["taskId"]
169168
):
170169
continue
171170
r.close()
172-
if task["type"] == "CustomCaptchaTask":
173-
payload["body"]["solution"] = payload["body"]["data"][0]
174171
job = Job(client=self, task_id=response["taskId"])
175172
job._last_result = payload["body"]
176173
return job

python_anticaptcha/compat.py

Lines changed: 0 additions & 15 deletions
This file was deleted.

python_anticaptcha/exceptions.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
class AnticaptchaException(Exception):
22
def __init__(self, error_id, error_code, error_description, *args):
3-
super(AnticaptchaException, self).__init__(
3+
super().__init__(
44
"[{}:{}]{}".format(error_code, error_id, error_description)
55
)
66
self.error_description = error_description
@@ -17,7 +17,7 @@ def __init__(self, width):
1717
msg = "Invalid width (%s). Can be one of these: 100, 50, 33, 25." % (
1818
self.width,
1919
)
20-
super(InvalidWidthException, self).__init__("AC-1", 1, msg)
20+
super().__init__("AC-1", 1, msg)
2121

2222

2323
class MissingNameException(AnticaptchaException):
@@ -26,4 +26,4 @@ def __init__(self, cls):
2626
msg = 'Missing name data in {0}. Provide {0}.__init__(name="X") or {0}.serialize(name="X")'.format(
2727
str(self.cls)
2828
)
29-
super(MissingNameException, self).__init__("AC-2", 2, msg)
29+
super().__init__("AC-2", 2, msg)

python_anticaptcha/tasks.py

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import base64
22

33

4-
class BaseTask(object):
4+
class BaseTask:
55
type = None
66

77
def serialize(self, **result):
@@ -12,21 +12,21 @@ def serialize(self, **result):
1212
class UserAgentMixin(BaseTask):
1313
def __init__(self, *args, **kwargs):
1414
self.userAgent = kwargs.pop("user_agent")
15-
super(UserAgentMixin, self).__init__(*args, **kwargs)
15+
super().__init__(*args, **kwargs)
1616

1717
def serialize(self, **result):
18-
data = super(UserAgentMixin, self).serialize(**result)
18+
data = super().serialize(**result)
1919
data["userAgent"] = self.userAgent
2020
return data
2121

2222

2323
class CookieMixin(BaseTask):
2424
def __init__(self, *args, **kwargs):
2525
self.cookies = kwargs.pop("cookies", "")
26-
super(CookieMixin, self).__init__(*args, **kwargs)
26+
super().__init__(*args, **kwargs)
2727

2828
def serialize(self, **result):
29-
data = super(CookieMixin, self).serialize(**result)
29+
data = super().serialize(**result)
3030
if self.cookies:
3131
data["cookies"] = self.cookies
3232
return data
@@ -39,10 +39,10 @@ def __init__(self, *args, **kwargs):
3939
self.proxyPort = kwargs.pop("proxy_port")
4040
self.proxyLogin = kwargs.pop("proxy_login")
4141
self.proxyPassword = kwargs.pop("proxy_password")
42-
super(ProxyMixin, self).__init__(*args, **kwargs)
42+
super().__init__(*args, **kwargs)
4343

4444
def serialize(self, **result):
45-
data = super(ProxyMixin, self).serialize(**result)
45+
data = super().serialize(**result)
4646
data["proxyType"] = self.proxyType
4747
data["proxyAddress"] = self.proxyAddress
4848
data["proxyPort"] = self.proxyPort
@@ -74,10 +74,10 @@ def __init__(
7474
self.websiteSToken = website_s_token
7575
self.recaptchaDataSValue = recaptcha_data_s_value
7676
self.isInvisible = is_invisible
77-
super(NoCaptchaTaskProxylessTask, self).__init__(*args, **kwargs)
77+
super().__init__(*args, **kwargs)
7878

7979
def serialize(self, **result):
80-
data = super(NoCaptchaTaskProxylessTask, self).serialize(**result)
80+
data = super().serialize(**result)
8181
data["websiteURL"] = self.websiteURL
8282
data["websiteKey"] = self.websiteKey
8383
if self.websiteSToken is not None:
@@ -117,10 +117,10 @@ def __init__(
117117
self.websiteKey = website_key
118118
self.funcaptchaApiJSSubdomain = subdomain
119119
self.data = data
120-
super(FunCaptchaProxylessTask, self).__init__(*args, **kwargs)
120+
super().__init__(*args, **kwargs)
121121

122122
def serialize(self, **result):
123-
data = super(FunCaptchaProxylessTask, self).serialize(**result)
123+
data = super().serialize(**result)
124124
data["websiteURL"] = self.websiteURL
125125
data["websitePublicKey"] = self.websiteKey
126126
if self.funcaptchaApiJSSubdomain:
@@ -157,7 +157,8 @@ def __init__(
157157
max_length=None,
158158
comment=None,
159159
website_url=None,
160-
*args, **kwargs
160+
*args,
161+
**kwargs
161162
):
162163
self.fp = fp
163164
self.phrase = phrase
@@ -168,19 +169,27 @@ def __init__(
168169
self.maxLength = max_length
169170
self.comment = comment
170171
self.websiteUrl = website_url
171-
super(ImageToTextTask, self).__init__(*args, **kwargs)
172+
super().__init__(*args, **kwargs)
172173

173174
def serialize(self, **result):
174-
data = super(ImageToTextTask, self).serialize(**result)
175+
data = super().serialize(**result)
175176
data["body"] = base64.b64encode(self.fp.read()).decode("utf-8")
176-
data["phrase"] = self.phrase
177-
data["case"] = self.case
178-
data["numeric"] = self.numeric
179-
data["math"] = self.math
180-
data["minLength"] = self.minLength
181-
data["maxLength"] = self.maxLength
182-
data["comment"] = self.comment
183-
data["websiteUrl"] = self.websiteUrl
177+
if self.phrase is not None:
178+
data["phrase"] = self.phrase
179+
if self.case is not None:
180+
data["case"] = self.case
181+
if self.numeric is not None:
182+
data["numeric"] = self.numeric
183+
if self.math is not None:
184+
data["math"] = self.math
185+
if self.minLength is not None:
186+
data["minLength"] = self.minLength
187+
if self.maxLength is not None:
188+
data["maxLength"] = self.maxLength
189+
if self.comment is not None:
190+
data["comment"] = self.comment
191+
if self.websiteUrl is not None:
192+
data["websiteUrl"] = self.websiteUrl
184193
return data
185194

186195

@@ -200,10 +209,10 @@ def __init__(
200209
self.minScore = min_score
201210
self.pageAction = page_action
202211
self.isEnterprise = is_enterprise
203-
super(RecaptchaV3TaskProxyless, self).__init__(*args, **kwargs)
212+
super().__init__(*args, **kwargs)
204213

205214
def serialize(self, **result):
206-
data = super(RecaptchaV3TaskProxyless, self).serialize(**result)
215+
data = super().serialize(**result)
207216
data["websiteURL"] = self.websiteURL
208217
data["websiteKey"] = self.websiteKey
209218
data["minScore"] = self.minScore
@@ -220,10 +229,10 @@ class HCaptchaTaskProxyless(BaseTask):
220229
def __init__(self, website_url, website_key, *args, **kwargs):
221230
self.websiteURL = website_url
222231
self.websiteKey = website_key
223-
super(HCaptchaTaskProxyless, self).__init__(*args, **kwargs)
232+
super().__init__(*args, **kwargs)
224233

225234
def serialize(self, **result):
226-
data = super(HCaptchaTaskProxyless, self).serialize(**result)
235+
data = super().serialize(**result)
227236
data["websiteURL"] = self.websiteURL
228237
data["websiteKey"] = self.websiteKey
229238
return data
@@ -245,10 +254,10 @@ def __init__(self, website_url, website_key, enterprise_payload, api_domain, *ar
245254
self.websiteKey = website_key
246255
self.enterprisePayload = enterprise_payload
247256
self.apiDomain = api_domain
248-
super(RecaptchaV2EnterpriseTaskProxyless, self).__init__(*args, **kwargs)
257+
super().__init__(*args, **kwargs)
249258

250259
def serialize(self, **result):
251-
data = super(RecaptchaV2EnterpriseTaskProxyless, self).serialize(**result)
260+
data = super().serialize(**result)
252261
data["websiteURL"] = self.websiteURL
253262
data["websiteKey"] = self.websiteKey
254263
if self.enterprisePayload:
@@ -258,7 +267,9 @@ def serialize(self, **result):
258267
return data
259268

260269

261-
class RecaptchaV2EnterpriseTask(ProxyMixin, UserAgentMixin, CookieMixin, BaseTask):
270+
class RecaptchaV2EnterpriseTask(
271+
ProxyMixin, UserAgentMixin, CookieMixin, RecaptchaV2EnterpriseTaskProxyless
272+
):
262273
type = "RecaptchaV2EnterpriseTask"
263274

264275

@@ -278,10 +289,10 @@ def __init__(
278289
self.challenge = challenge
279290
self.geetestApiServerSubdomain = subdomain
280291
self.geetestGetLib = lib
281-
super(GeeTestTaskProxyless, self).__init__(*args, **kwargs)
292+
super().__init__(*args, **kwargs)
282293

283294
def serialize(self, **result):
284-
data = super(GeeTestTaskProxyless, self).serialize(**result)
295+
data = super().serialize(**result)
285296
data["websiteURL"] = self.websiteURL
286297
data["gt"] = self.gt
287298
data["challenge"] = self.challenge
@@ -306,10 +317,10 @@ def __init__(self, website_url, template_name, variables, *args, **kwargs):
306317
self.websiteURL = website_url
307318
self.templateName = template_name
308319
self.variables = variables
309-
super(AntiGateTaskProxyless).__init__(self, *args, **kwargs)
320+
super().__init__(*args, **kwargs)
310321

311322
def serialize(self, **result):
312-
data = super(AntiGateTaskProxyless, self).serialize(**result)
323+
data = super().serialize(**result)
313324
data["websiteURL"] = self.websiteURL
314325
data["templateName"] = self.templateName
315326
data["variables"] = self.variables

setup.cfg

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,3 @@ tag_name = {new_version}
66

77
[nosetests]
88
process-timeout = 600
9-
10-
[bdist_wheel]
11-
universal = 1
12-

0 commit comments

Comments
 (0)