diff --git a/Makefile b/Makefile index e4fbfbea..19d48c01 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ lint: isort src/ --check-only upload: - pip3 install twine + pip3 install twine wheel cd src/ && python setup.py upload tests: install diff --git a/docs/_static/RuCaptchaMedium.png b/docs/_static/RuCaptchaMedium.png index fbedef3b..621858f8 100644 Binary files a/docs/_static/RuCaptchaMedium.png and b/docs/_static/RuCaptchaMedium.png differ diff --git a/docs/index.rst b/docs/index.rst index d8630ede..d692d073 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -40,6 +40,7 @@ Check our other projects here - `RedPandaDev group RuCaptchaRuCaptcha2Captcha2Captcha Union[dict, Exception]: + def _processing_response(self, **kwargs: dict) -> dict: """ Method processing captcha solving task creation result :param kwargs: additional params for Requests library @@ -71,7 +70,10 @@ def _processing_response(self, **kwargs: dict) -> Union[dict, Exception]: else: return response.to_dict() except Exception as error: - return error + self.result.errorId = 12 + self.result.errorCode = self.NO_CAPTCHA_ERR + self.result.errorDescription = str(error) + return self.result.to_dict() # wait captcha solving time.sleep(self.params.sleep_time) @@ -96,7 +98,7 @@ async def aio_url_read(self, url: str, **kwargs) -> bytes: async with session.get(url=url, **kwargs) as resp: return await resp.content.read() - async def _aio_processing_response(self) -> Union[dict, Exception]: + async def _aio_processing_response(self) -> dict: """ Method processing async captcha solving task creation result """ @@ -109,7 +111,10 @@ async def _aio_processing_response(self) -> Union[dict, Exception]: else: return response.to_dict() except Exception as error: - return error + self.result.errorId = 12 + self.result.errorCode = self.NO_CAPTCHA_ERR + self.result.errorDescription = str(error) + return self.result.to_dict() # wait captcha solving await asyncio.sleep(self.params.sleep_time) diff --git a/src/python_rucaptcha/core/enums.py b/src/python_rucaptcha/core/enums.py index ca3fa916..482d3065 100644 --- a/src/python_rucaptcha/core/enums.py +++ b/src/python_rucaptcha/core/enums.py @@ -109,3 +109,8 @@ class TextCaptchaEnm(str, MyEnum): class AudioCaptchaEnm(str, MyEnum): AudioTask = "AudioTask" + + +class CutCaptchaEnm(str, MyEnum): + CutCaptchaTask = "CutCaptchaTask" + CutCaptchaTaskProxyless = "CutCaptchaTaskProxyless" diff --git a/src/python_rucaptcha/cutcaptcha.py b/src/python_rucaptcha/cutcaptcha.py new file mode 100644 index 00000000..8be9a152 --- /dev/null +++ b/src/python_rucaptcha/cutcaptcha.py @@ -0,0 +1,116 @@ +from typing import Union + +from .core.base import BaseCaptcha +from .core.enums import CutCaptchaEnm + + +class CutCaptcha(BaseCaptcha): + def __init__( + self, + websiteURL: str, + miseryKey: str, + apiKey: str, + method: Union[str, CutCaptchaEnm] = CutCaptchaEnm.CutCaptchaTaskProxyless, + *args, + **kwargs, + ): + """ + The class is used to work with CutCaptcha. + + Args: + rucaptcha_key: User API key + websiteURL: Full URL of the captcha page + miseryKey: The value of CUTCAPTCHA_MISERY_KEY variable defined on page. + apiKey: The value of data-apikey attribute of iframe's body. + Also the name of javascript file included on the page + method: Captcha type + kwargs: Not required params for task creation request + + Examples: + >>> CutCaptcha(rucaptcha_key="aa9011f31111181111168611f1151122", + ... websiteURL="https://example.cc/foo/bar.html", + ... miseryKey="a1488b66da00bf332a1488993a5443c79047e752", + ... apiKey="SAb83IIB", + ... method=CutCaptchaEnm.CutCaptchaTaskProxyless + ... ).captcha_handler() + { + "errorId":0, + "status":"ready", + "solution":{ + "token":"P1_eyJ0eXAiOiJKV...1LDq89KyJ5A", + "respKey":"E0_eyJ0eXAiOiJK...y2w5_YbP8PGuJBBo", + "userAgent":"Mozilla/5.0 (.......", + "gRecaptchaResponse":"P1_eyJ0eXAiOiJKV...1LDq89KyJ5A" + }, + "cost":"0.00299", + "ip":"1.2.3.4", + "createTime":1692863536, + "endTime":1692863556, + "solveCount":1, + "taskId": 73243152973, + } + + >>> await CutCaptcha(rucaptcha_key="aa9011f31111181111168611f1151122", + ... websiteURL="https://example.cc/foo/bar.html", + ... miseryKey="a1488b66da00bf332a1488993a5443c79047e752", + ... apiKey="SAb83IIB", + ... method=CutCaptchaEnm.CutCaptchaTaskProxyless + ... ).aio_captcha_handler() + { + "errorId":0, + "status":"ready", + "solution":{ + "token":"P1_eyJ0eXAiOiJKV...1LDq89KyJ5A", + "respKey":"E0_eyJ0eXAiOiJK...y2w5_YbP8PGuJBBo", + "userAgent":"Mozilla/5.0 (........", + "gRecaptchaResponse":"P1_eyJ0eXAiOiJKV...1LDq89KyJ5A" + }, + "cost":"0.00299", + "ip":"1.2.3.4", + "createTime":1692863536, + "endTime":1692863556, + "solveCount":1, + "taskId": 73243152973, + } + + Returns: + Dict with full server response + + Notes: + https://2captcha.com/api-docs/cutcaptcha + """ + super().__init__(method=method, *args, **kwargs) + + self.create_task_payload["task"].update({"websiteURL": websiteURL, "miseryKey": miseryKey, "apiKey": apiKey}) + + # check user params + if method not in CutCaptchaEnm.list_values(): + raise ValueError(f"Invalid method parameter set, available - {CutCaptchaEnm.list_values()}") + + def captcha_handler(self, **kwargs) -> dict: + """ + Sync solving method + + Args: + kwargs: Parameters for the `requests` library + + Returns: + Dict with full server response + + Notes: + Check class docstirng for more info + """ + + return self._processing_response(**kwargs) + + async def aio_captcha_handler(self) -> dict: + """ + Async solving method + + Returns: + Dict with full server response + + Notes: + Check class docstirng for more info + """ + return await self._aio_processing_response() diff --git a/src/setup.py b/src/setup.py index f2e1117c..b9c2cba2 100644 --- a/src/setup.py +++ b/src/setup.py @@ -11,7 +11,7 @@ # Package meta-data. NAME = "python-rucaptcha" -DESCRIPTION = "Python 3.7+ RuCaptcha library with AIO module." +DESCRIPTION = "Python 3.9+ RuCaptcha library with AIO module." URL = "https://andreidrang.github.io/python-rucaptcha/" EMAIL = "python-captcha@pm.me" AUTHOR = "AndreiDrang, redV0ID" diff --git a/tests/test_core.py b/tests/test_core.py index 4038cc94..361943be 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -4,7 +4,7 @@ from tests.conftest import BaseTest from python_rucaptcha.core.base import BaseCaptcha -from python_rucaptcha.core.enums import MyEnum, ControlEnm +from python_rucaptcha.core.enums import MyEnum, ControlEnm, ServiceEnm from python_rucaptcha.core.config import RETRIES, ASYNC_RETRIES, attempts_generator @@ -104,3 +104,12 @@ def test_attempts_generator(self): for attempt in attempts: assert isinstance(attempt, int) assert attempt == 4 + + +class TestDeathbycaptcha(BaseTest): + def test_attempts_generator(self): + BaseCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + service_type=ServiceEnm.DEATHBYCAPTCHA.value, + method=ControlEnm.control.value, + ) diff --git a/tests/test_cutcaptcha.py b/tests/test_cutcaptcha.py new file mode 100644 index 00000000..9554d355 --- /dev/null +++ b/tests/test_cutcaptcha.py @@ -0,0 +1,158 @@ +import pytest + +from tests.conftest import BaseTest +from python_rucaptcha.core.enums import CutCaptchaEnm +from python_rucaptcha.cutcaptcha import CutCaptcha +from python_rucaptcha.core.serializer import GetTaskResultResponseSer + + +class TestCutCaptcha(BaseTest): + miseryKey = "a1488b66da00bf332a1488993a5443c79047e752" + pageurl = "https://example.cc/foo/bar.html" + apiKey = "SAb83IIB" + + kwargs_params = { + "proxyType": "socks5", + "proxyAddress": BaseTest.proxyAddress, + "proxyPort": BaseTest.proxyPort, + } + + def test_methods_exists(self): + assert "captcha_handler" in CutCaptcha.__dict__.keys() + assert "aio_captcha_handler" in CutCaptcha.__dict__.keys() + + @pytest.mark.parametrize("method", CutCaptchaEnm.list_values()) + def test_args(self, method: str): + instance = CutCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.pageurl, + miseryKey=self.miseryKey, + apiKey=self.apiKey, + method=method, + ) + assert instance.create_task_payload["clientKey"] == self.RUCAPTCHA_KEY + assert instance.create_task_payload["task"]["type"] == method + assert instance.create_task_payload["task"]["websiteURL"] == self.pageurl + assert instance.create_task_payload["task"]["miseryKey"] == self.miseryKey + assert instance.create_task_payload["task"]["apiKey"] == self.apiKey + + def test_kwargs(self): + instance = CutCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.pageurl, + miseryKey=self.miseryKey, + apiKey=self.apiKey, + method=CutCaptchaEnm.CutCaptchaTaskProxyless, + **self.kwargs_params, + ) + assert set(self.kwargs_params.keys()).issubset(set(instance.create_task_payload["task"].keys())) + assert set(self.kwargs_params.values()).issubset(set(instance.create_task_payload["task"].values())) + + """ + Success tests + """ + + def test_basic_data(self): + instance = CutCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.pageurl, + miseryKey=self.miseryKey, + apiKey=self.apiKey, + method=CutCaptchaEnm.CutCaptchaTaskProxyless.value, + ) + + result = instance.captcha_handler() + + assert isinstance(result, dict) is True + if not result["errorId"]: + assert result["status"] == "ready" + assert isinstance(result["solution"], dict) is True + assert isinstance(result["taskId"], int) is True + else: + assert result["errorId"] in (1, 12) + assert result["errorCode"] == "ERROR_CAPTCHA_UNSOLVABLE" + + assert result.keys() == GetTaskResultResponseSer().to_dict().keys() + + async def test_aio_basic_data(self): + instance = CutCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.pageurl, + miseryKey=self.miseryKey, + apiKey=self.apiKey, + method=CutCaptchaEnm.CutCaptchaTaskProxyless.value, + ) + + result = await instance.aio_captcha_handler() + + assert isinstance(result, dict) is True + if not result["errorId"]: + assert result["status"] == "ready" + assert isinstance(result["solution"], dict) is True + assert isinstance(result["taskId"], int) is True + else: + assert result["errorId"] in (1, 12) + assert result["errorCode"] in ("ERROR_CAPTCHA_UNSOLVABLE", CutCaptcha.NO_CAPTCHA_ERR) + + assert result.keys() == GetTaskResultResponseSer().to_dict().keys() + + def test_context_basic_data(self): + with CutCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.pageurl, + miseryKey=self.miseryKey, + apiKey=self.apiKey, + method=CutCaptchaEnm.CutCaptchaTaskProxyless.value, + ) as instance: + assert instance.captcha_handler() + + async def test_context_aio_basic_data(self): + async with CutCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.pageurl, + miseryKey=self.miseryKey, + apiKey=self.apiKey, + method=CutCaptchaEnm.CutCaptchaTaskProxyless.value, + ) as instance: + assert await instance.aio_captcha_handler() + + """ + Fail tests + """ + + def test_wrong_method(self): + with pytest.raises(ValueError): + CutCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.pageurl, + miseryKey=self.miseryKey, + apiKey=self.apiKey, + method=self.get_random_string(length=5), + ) + + def test_no_websiteURL(self): + with pytest.raises(TypeError): + CutCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + miseryKey=self.miseryKey, + apiKey=self.apiKey, + method=self.get_random_string(length=5), + ) + + def test_no_miseryKey(self): + with pytest.raises(TypeError): + CutCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.pageurl, + apiKey=self.apiKey, + method=self.get_random_string(length=5), + ) + + def test_no_apiKey(self): + with pytest.raises(TypeError): + CutCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.pageurl, + miseryKey=self.miseryKey, + method=self.get_random_string(length=5), + )