From b44c67ee09a974733ea7fb27ba1df37e64f50ba8 Mon Sep 17 00:00:00 2001 From: Adam Dobrawy Date: Sat, 7 Mar 2026 12:21:00 +0100 Subject: [PATCH] Drop Python 2 support, require Python >= 3.9, modernize codebase - 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: Claude Opus 4.6 --- .github/workflows/e2e.yml | 15 +++--- .github/workflows/pythonpackage.yml | 18 +++---- python_anticaptcha/__init__.py | 9 ++-- python_anticaptcha/base.py | 11 ++--- python_anticaptcha/compat.py | 15 ------ python_anticaptcha/exceptions.py | 6 +-- python_anticaptcha/tasks.py | 77 ++++++++++++++++------------- setup.cfg | 4 -- setup.py | 22 ++++----- tox.ini | 10 ++-- 10 files changed, 82 insertions(+), 105 deletions(-) delete mode 100644 python_anticaptcha/compat.py diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 7d3c470..df94488 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -11,17 +11,20 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - - name: Set up Python 3.7 - uses: actions/setup-python@v1 + - name: Set up Python 3.12 + uses: actions/setup-python@v5 with: - python-version: "3.7" + python-version: "3.12" + + - name: Install setup dependencies + run: pip install setuptools_scm wheel - name: Build distribution run: make install - - - uses: nanasess/setup-chromedriver@v1 + + - uses: nanasess/setup-chromedriver@v2 - name: Run integration tests run: make test diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index f8b464f..e8c0537 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -11,19 +11,18 @@ jobs: fail-fast: false matrix: python-version: - - '2.7' - - '3.5' - - '3.6' - - '3.7' - - '3.8' - '3.9' - '3.10' + - '3.11' + - '3.12' + - '3.13' + - '3.14' steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -36,8 +35,5 @@ jobs: - name: Build docs run: make docs - - name: Build docs - run: make docs - - name: Test with pytest - run: make test \ No newline at end of file + run: make test diff --git a/python_anticaptcha/__init__.py b/python_anticaptcha/__init__.py index f73e46d..80213de 100644 --- a/python_anticaptcha/__init__.py +++ b/python_anticaptcha/__init__.py @@ -1,9 +1,6 @@ +from importlib.metadata import version, PackageNotFoundError + from .base import AnticaptchaClient -try: - from importlib.metadata import version, PackageNotFoundError -except ImportError: - # Python < 3.8 fallback - from importlib_metadata import version, PackageNotFoundError from .tasks import ( NoCaptchaTaskProxylessTask, RecaptchaV2TaskProxyless, @@ -20,7 +17,7 @@ GeeTestTaskProxyless, GeeTestTask, AntiGateTaskProxyless, - AntiGateTask + AntiGateTask, ) from .exceptions import AnticaptchaException diff --git a/python_anticaptcha/base.py b/python_anticaptcha/base.py index db654d3..5753deb 100644 --- a/python_anticaptcha/base.py +++ b/python_anticaptcha/base.py @@ -3,15 +3,14 @@ import json import warnings -from .compat import split -from six.moves.urllib_parse import urljoin +from urllib.parse import urljoin from .exceptions import AnticaptchaException SLEEP_EVERY_CHECK_FINISHED = 3 MAXIMUM_JOIN_TIME = 60 * 5 -class Job(object): +class Job: client = None task_id = None _last_result = None @@ -74,7 +73,7 @@ def join(self, maximum_time=None): ) -class AnticaptchaClient(object): +class AnticaptchaClient: client_key = None CREATE_TASK_URL = "/createTask" TASK_RESULT_URL = "/getTaskResult" @@ -163,14 +162,12 @@ def createTaskSmee(self, task, timeout=MAXIMUM_JOIN_TIME): content = line.decode("utf-8") if '"host":"smee.io"' not in content: continue - payload = json.loads(split(content, ":", 1)[1].strip()) + payload = json.loads(content.split(":", maxsplit=1)[1].strip()) if "taskId" not in payload["body"] or str(payload["body"]["taskId"]) != str( response["taskId"] ): continue r.close() - if task["type"] == "CustomCaptchaTask": - payload["body"]["solution"] = payload["body"]["data"][0] job = Job(client=self, task_id=response["taskId"]) job._last_result = payload["body"] return job diff --git a/python_anticaptcha/compat.py b/python_anticaptcha/compat.py deleted file mode 100644 index c0c5c6f..0000000 --- a/python_anticaptcha/compat.py +++ /dev/null @@ -1,15 +0,0 @@ -import six - -if six.PY3: - - def split(value, sep, maxsplit): - return value.split(sep, maxsplit=maxsplit) - - -else: - - def split(value, sep, maxsplit): - parts = value.split(sep) - return parts[:maxsplit] + [ - sep.join(parts[maxsplit:]), - ] diff --git a/python_anticaptcha/exceptions.py b/python_anticaptcha/exceptions.py index dccad3b..108febd 100644 --- a/python_anticaptcha/exceptions.py +++ b/python_anticaptcha/exceptions.py @@ -1,6 +1,6 @@ class AnticaptchaException(Exception): def __init__(self, error_id, error_code, error_description, *args): - super(AnticaptchaException, self).__init__( + super().__init__( "[{}:{}]{}".format(error_code, error_id, error_description) ) self.error_description = error_description @@ -17,7 +17,7 @@ def __init__(self, width): msg = "Invalid width (%s). Can be one of these: 100, 50, 33, 25." % ( self.width, ) - super(InvalidWidthException, self).__init__("AC-1", 1, msg) + super().__init__("AC-1", 1, msg) class MissingNameException(AnticaptchaException): @@ -26,4 +26,4 @@ def __init__(self, cls): msg = 'Missing name data in {0}. Provide {0}.__init__(name="X") or {0}.serialize(name="X")'.format( str(self.cls) ) - super(MissingNameException, self).__init__("AC-2", 2, msg) + super().__init__("AC-2", 2, msg) diff --git a/python_anticaptcha/tasks.py b/python_anticaptcha/tasks.py index 8fe64c6..3ef6890 100644 --- a/python_anticaptcha/tasks.py +++ b/python_anticaptcha/tasks.py @@ -1,7 +1,7 @@ import base64 -class BaseTask(object): +class BaseTask: type = None def serialize(self, **result): @@ -12,10 +12,10 @@ def serialize(self, **result): class UserAgentMixin(BaseTask): def __init__(self, *args, **kwargs): self.userAgent = kwargs.pop("user_agent") - super(UserAgentMixin, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def serialize(self, **result): - data = super(UserAgentMixin, self).serialize(**result) + data = super().serialize(**result) data["userAgent"] = self.userAgent return data @@ -23,10 +23,10 @@ def serialize(self, **result): class CookieMixin(BaseTask): def __init__(self, *args, **kwargs): self.cookies = kwargs.pop("cookies", "") - super(CookieMixin, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def serialize(self, **result): - data = super(CookieMixin, self).serialize(**result) + data = super().serialize(**result) if self.cookies: data["cookies"] = self.cookies return data @@ -39,10 +39,10 @@ def __init__(self, *args, **kwargs): self.proxyPort = kwargs.pop("proxy_port") self.proxyLogin = kwargs.pop("proxy_login") self.proxyPassword = kwargs.pop("proxy_password") - super(ProxyMixin, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def serialize(self, **result): - data = super(ProxyMixin, self).serialize(**result) + data = super().serialize(**result) data["proxyType"] = self.proxyType data["proxyAddress"] = self.proxyAddress data["proxyPort"] = self.proxyPort @@ -74,10 +74,10 @@ def __init__( self.websiteSToken = website_s_token self.recaptchaDataSValue = recaptcha_data_s_value self.isInvisible = is_invisible - super(NoCaptchaTaskProxylessTask, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def serialize(self, **result): - data = super(NoCaptchaTaskProxylessTask, self).serialize(**result) + data = super().serialize(**result) data["websiteURL"] = self.websiteURL data["websiteKey"] = self.websiteKey if self.websiteSToken is not None: @@ -117,10 +117,10 @@ def __init__( self.websiteKey = website_key self.funcaptchaApiJSSubdomain = subdomain self.data = data - super(FunCaptchaProxylessTask, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def serialize(self, **result): - data = super(FunCaptchaProxylessTask, self).serialize(**result) + data = super().serialize(**result) data["websiteURL"] = self.websiteURL data["websitePublicKey"] = self.websiteKey if self.funcaptchaApiJSSubdomain: @@ -157,7 +157,8 @@ def __init__( max_length=None, comment=None, website_url=None, - *args, **kwargs + *args, + **kwargs ): self.fp = fp self.phrase = phrase @@ -168,19 +169,27 @@ def __init__( self.maxLength = max_length self.comment = comment self.websiteUrl = website_url - super(ImageToTextTask, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def serialize(self, **result): - data = super(ImageToTextTask, self).serialize(**result) + data = super().serialize(**result) data["body"] = base64.b64encode(self.fp.read()).decode("utf-8") - data["phrase"] = self.phrase - data["case"] = self.case - data["numeric"] = self.numeric - data["math"] = self.math - data["minLength"] = self.minLength - data["maxLength"] = self.maxLength - data["comment"] = self.comment - data["websiteUrl"] = self.websiteUrl + if self.phrase is not None: + data["phrase"] = self.phrase + if self.case is not None: + data["case"] = self.case + if self.numeric is not None: + data["numeric"] = self.numeric + if self.math is not None: + data["math"] = self.math + if self.minLength is not None: + data["minLength"] = self.minLength + if self.maxLength is not None: + data["maxLength"] = self.maxLength + if self.comment is not None: + data["comment"] = self.comment + if self.websiteUrl is not None: + data["websiteUrl"] = self.websiteUrl return data @@ -200,10 +209,10 @@ def __init__( self.minScore = min_score self.pageAction = page_action self.isEnterprise = is_enterprise - super(RecaptchaV3TaskProxyless, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def serialize(self, **result): - data = super(RecaptchaV3TaskProxyless, self).serialize(**result) + data = super().serialize(**result) data["websiteURL"] = self.websiteURL data["websiteKey"] = self.websiteKey data["minScore"] = self.minScore @@ -220,10 +229,10 @@ class HCaptchaTaskProxyless(BaseTask): def __init__(self, website_url, website_key, *args, **kwargs): self.websiteURL = website_url self.websiteKey = website_key - super(HCaptchaTaskProxyless, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def serialize(self, **result): - data = super(HCaptchaTaskProxyless, self).serialize(**result) + data = super().serialize(**result) data["websiteURL"] = self.websiteURL data["websiteKey"] = self.websiteKey return data @@ -245,10 +254,10 @@ def __init__(self, website_url, website_key, enterprise_payload, api_domain, *ar self.websiteKey = website_key self.enterprisePayload = enterprise_payload self.apiDomain = api_domain - super(RecaptchaV2EnterpriseTaskProxyless, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def serialize(self, **result): - data = super(RecaptchaV2EnterpriseTaskProxyless, self).serialize(**result) + data = super().serialize(**result) data["websiteURL"] = self.websiteURL data["websiteKey"] = self.websiteKey if self.enterprisePayload: @@ -258,7 +267,9 @@ def serialize(self, **result): return data -class RecaptchaV2EnterpriseTask(ProxyMixin, UserAgentMixin, CookieMixin, BaseTask): +class RecaptchaV2EnterpriseTask( + ProxyMixin, UserAgentMixin, CookieMixin, RecaptchaV2EnterpriseTaskProxyless +): type = "RecaptchaV2EnterpriseTask" @@ -278,10 +289,10 @@ def __init__( self.challenge = challenge self.geetestApiServerSubdomain = subdomain self.geetestGetLib = lib - super(GeeTestTaskProxyless, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def serialize(self, **result): - data = super(GeeTestTaskProxyless, self).serialize(**result) + data = super().serialize(**result) data["websiteURL"] = self.websiteURL data["gt"] = self.gt data["challenge"] = self.challenge @@ -306,10 +317,10 @@ def __init__(self, website_url, template_name, variables, *args, **kwargs): self.websiteURL = website_url self.templateName = template_name self.variables = variables - super(AntiGateTaskProxyless).__init__(self, *args, **kwargs) + super().__init__(*args, **kwargs) def serialize(self, **result): - data = super(AntiGateTaskProxyless, self).serialize(**result) + data = super().serialize(**result) data["websiteURL"] = self.websiteURL data["templateName"] = self.templateName data["variables"] = self.variables diff --git a/setup.cfg b/setup.cfg index ac7a798..12db776 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,7 +6,3 @@ tag_name = {new_version} [nosetests] process-timeout = 600 - -[bdist_wheel] -universal = 1 - diff --git a/setup.py b/setup.py index a9862ef..8388db0 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,8 @@ from setuptools import setup -from codecs import open -from os import path, system -import sys +from os import path here = path.abspath(path.dirname(__file__)) -# Get the long description from the README file with open(path.join(here, "README.rst"), encoding="utf-8") as f: long_description = f.read() @@ -15,34 +12,33 @@ extras = {"tests": tests_deps, "docs": "sphinx"} setup( - test_suite='nose2.collector.collector', + test_suite="nose2.collector.collector", name="python-anticaptcha", description="Client library for solve captchas with Anticaptcha.com support.", long_description=long_description, + long_description_content_type="text/x-rst", url="https://github.com/ad-m/python-anticaptcha", author="Adam Dobrawy", author_email="anticaptcha@jawnosc.tk", license="MIT", + python_requires=">=3.9", classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ], use_scm_version=True, setup_requires=["setuptools_scm", "wheel"], keywords="recaptcha captcha development", packages=["python_anticaptcha"], - install_requires=["requests", "six"], + install_requires=["requests"], tests_require=tests_deps, extras_require=extras, ) diff --git a/tox.ini b/tox.ini index 4b33dfb..802ff7d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,8 @@ [tox] -envlist = py27,py34,py35,pypy,pypy3 +envlist = py39,py310,py311,py312,py313,py314 [testenv] passenv=KEY PROXY_URL -deps=. +deps=.[tests] commands= - - python examples/recaptcha.py - - python examples/text.py - -[bdist_wheel] -universal=1 + nose2 --verbose