diff --git a/docs/conf.py b/docs/conf.py index 33348f1c..66dc8526 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,6 +21,7 @@ lemin_captcha, rotate_captcha, datadome_captcha, + friendly_captcha, cyber_siara_captcha, draw_around_captcha, bounding_box_captcha, diff --git a/docs/modules/enum/info.rst b/docs/modules/enum/info.rst index 18eebd3c..5579ff9f 100644 --- a/docs/modules/enum/info.rst +++ b/docs/modules/enum/info.rst @@ -76,3 +76,6 @@ To import this module: .. autoenum:: python_rucaptcha.core.enums.GridCaptchaEnm :members: + +.. autoenum:: python_rucaptcha.core.enums.FriendlyCaptchaEnm + :members: diff --git a/docs/modules/friendly-captcha/example.rst b/docs/modules/friendly-captcha/example.rst new file mode 100644 index 00000000..f1872c00 --- /dev/null +++ b/docs/modules/friendly-captcha/example.rst @@ -0,0 +1,12 @@ +FriendlyCaptcha +=============== + +To import this module: + +.. code-block:: python + + from python_rucaptcha.friendly_captcha import FriendlyCaptcha + + +.. autoclass:: python_rucaptcha.friendly_captcha.FriendlyCaptcha + :members: \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index c1ff2541..07ec7697 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,6 +2,6 @@ sphinx==7.2.6 pallets_sphinx_themes==2.1.1 myst-parser==2.0.0 autodoc_pydantic==2.0.1 -pydantic==2.5.2 +pydantic==2.5.3 pydantic-settings==2.1.0 enum-tools[sphinx]==0.11.0 diff --git a/requirements.style.txt b/requirements.style.txt index 48f83b59..df939544 100644 --- a/requirements.style.txt +++ b/requirements.style.txt @@ -1,4 +1,4 @@ # codestyle isort==5.* -black==23.10.0 +black==23.12.0 autoflake==2.* diff --git a/src/python_rucaptcha/__version__.py b/src/python_rucaptcha/__version__.py index 8833022e..0237e6dc 100644 --- a/src/python_rucaptcha/__version__.py +++ b/src/python_rucaptcha/__version__.py @@ -1 +1 @@ -__version__ = "6.1" +__version__ = "6.1.2" diff --git a/src/python_rucaptcha/core/enums.py b/src/python_rucaptcha/core/enums.py index 1ca00112..1d999f14 100644 --- a/src/python_rucaptcha/core/enums.py +++ b/src/python_rucaptcha/core/enums.py @@ -144,3 +144,8 @@ class CoordinatesCaptchaEnm(str, MyEnum): class GridCaptchaEnm(str, MyEnum): GridTask = "GridTask" + + +class FriendlyCaptchaEnm(str, MyEnum): + FriendlyCaptchaTaskProxyless = "FriendlyCaptchaTaskProxyless" + FriendlyCaptchaTask = "FriendlyCaptchaTask" diff --git a/src/python_rucaptcha/friendly_captcha.py b/src/python_rucaptcha/friendly_captcha.py new file mode 100644 index 00000000..472c6542 --- /dev/null +++ b/src/python_rucaptcha/friendly_captcha.py @@ -0,0 +1,124 @@ +from typing import Union + +from .core.base import BaseCaptcha +from .core.enums import FriendlyCaptchaEnm + + +class FriendlyCaptcha(BaseCaptcha): + def __init__( + self, + websiteURL: str, + websiteKey: str, + method: Union[str, FriendlyCaptchaEnm] = FriendlyCaptchaEnm.FriendlyCaptchaTaskProxyless, + *args, + **kwargs, + ): + """ + The class is used to work with Friendly Captcha. + + Args: + rucaptcha_key: User API key + websiteURL: The full URL of target web page where the captcha is loaded. We do not open the page, + not a problem if it is available only for authenticated users + websiteKey: The value of `data-sitekey` attribute of captcha's `div` element on page. + method: Captcha type + + Examples: + >>> FriendlyCaptcha(rucaptcha_key="aa9011f31111181111168611f1151122", + ... websiteKey="2FZFEVS1FZCGQ9", + ... websiteURL="https://example.com", + ... method=FriendlyCaptchaEnm.FriendlyCaptchaTaskProxyless.value + ... ).captcha_handler() + { + "errorId":0, + "status":"ready", + "solution":{ + "token":"PUZZLE_Abc1dEFghIJKLM2no34P56q7rStu8v" + }, + "cost":"0.00299", + "ip":"1.2.3.4", + "createTime":1692863536, + "endTime":1692863556, + "solveCount":1, + "taskId":75190409731 + } + + >>> FriendlyCaptcha(rucaptcha_key="aa9011f31111181111168611f1151122", + ... websiteKey="2FZFEVS1FZCGQ9", + ... websiteURL="https://example.com", + ... method=FriendlyCaptchaEnm.FriendlyCaptchaTaskProxyless.value + ... ).captcha_handler() + { + "errorId":0, + "status":"ready", + "solution":{ + "token":"PUZZLE_Abc1dEFghIJKLM2no34P56q7rStu8v" + }, + "cost":"0.00299", + "ip":"1.2.3.4", + "createTime":1692863536, + "endTime":1692863556, + "solveCount":1, + "taskId":75190409731 + } + + >>> await FriendlyCaptcha(rucaptcha_key="aa9011f31111181111168611f1151122", + ... websiteKey="2FZFEVS1FZCGQ9", + ... websiteURL="https://example.com", + ... method=FriendlyCaptchaEnm.FriendlyCaptchaTaskProxyless.value + ... ).aio_captcha_handler() + { + "errorId":0, + "status":"ready", + "solution":{ + "token":"PUZZLE_Abc1dEFghIJKLM2no34P56q7rStu8v" + }, + "cost":"0.00299", + "ip":"1.2.3.4", + "createTime":1692863536, + "endTime":1692863556, + "solveCount":1, + "taskId":75190409731 + } + + Returns: + Dict with full server response + + Notes: + https://rucaptcha.com/api-docs/friendly-captcha + """ + super().__init__(method=method, *args, **kwargs) + + self.create_task_payload["task"].update({"websiteURL": websiteURL, "websiteKey": websiteKey}) + + # check user params + if method not in FriendlyCaptchaEnm.list_values(): + raise ValueError(f"Invalid method parameter set, available - {FriendlyCaptchaEnm.list_values()}") + + def captcha_handler(self, **kwargs) -> dict: + """ + Sync solving method + + Args: + kwargs: additional params for `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 b9c2cba2..8f5cfe52 100644 --- a/src/setup.py +++ b/src/setup.py @@ -115,6 +115,7 @@ def run(self): turnstile amazon amazon_waf + friendly-captcha """, python_requires=REQUIRES_PYTHON, zip_safe=False, diff --git a/tests/test_friendly_captcha.py b/tests/test_friendly_captcha.py new file mode 100644 index 00000000..717c27c1 --- /dev/null +++ b/tests/test_friendly_captcha.py @@ -0,0 +1,131 @@ +import pytest + +from tests.conftest import BaseTest +from python_rucaptcha.core.enums import FriendlyCaptchaEnm +from python_rucaptcha.core.serializer import GetTaskResultResponseSer +from python_rucaptcha.friendly_captcha import FriendlyCaptcha + + +class TestFriendlyCaptcha(BaseTest): + websiteURL = "https://example.cc/foo/bar.html" + websiteKey = "SAb83IIB" + + kwargs_params = { + "proxyType": "socks5", + "proxyAddress": BaseTest.proxyAddress, + "proxyPort": BaseTest.proxyPort, + } + + def test_methods_exists(self): + assert "captcha_handler" in FriendlyCaptcha.__dict__.keys() + assert "aio_captcha_handler" in FriendlyCaptcha.__dict__.keys() + + @pytest.mark.parametrize("method", FriendlyCaptchaEnm.list_values()) + def test_args(self, method: str): + instance = FriendlyCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + 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.websiteURL + assert instance.create_task_payload["task"]["websiteKey"] == self.websiteKey + + def test_kwargs(self): + instance = FriendlyCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=FriendlyCaptchaEnm.FriendlyCaptchaTaskProxyless, + **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 = FriendlyCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=FriendlyCaptchaEnm.FriendlyCaptchaTaskProxyless.value, + ) + + result = instance.captcha_handler() + + assert isinstance(result, dict) is True + if not result["errorId"]: + assert result["status"] in ("ready", "processing") + 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 = FriendlyCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=FriendlyCaptchaEnm.FriendlyCaptchaTaskProxyless.value, + ) + + result = await instance.aio_captcha_handler() + + assert isinstance(result, dict) is True + if not result["errorId"]: + assert result["status"] in ("ready", "processing") + assert isinstance(result["taskId"], int) is True + else: + assert result["errorId"] in (1, 12) + assert result["errorCode"] in ("ERROR_CAPTCHA_UNSOLVABLE", FriendlyCaptcha.NO_CAPTCHA_ERR) + + assert result.keys() == GetTaskResultResponseSer().to_dict().keys() + + def test_context_basic_data(self): + with FriendlyCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=FriendlyCaptchaEnm.FriendlyCaptchaTaskProxyless.value, + ) as instance: + assert instance + + async def test_context_aio_basic_data(self): + async with FriendlyCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=FriendlyCaptchaEnm.FriendlyCaptchaTaskProxyless.value, + ) as instance: + assert instance + + """ + Fail tests + """ + + def test_wrong_method(self): + with pytest.raises(ValueError): + FriendlyCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + websiteKey=self.websiteKey, + method=self.get_random_string(length=5), + ) + + def test_no_websiteURL(self): + with pytest.raises(TypeError): + FriendlyCaptcha(rucaptcha_key=self.RUCAPTCHA_KEY, websiteKey=self.websiteKey) + + def test_no_websiteKey(self): + with pytest.raises(TypeError): + FriendlyCaptcha( + rucaptcha_key=self.RUCAPTCHA_KEY, + websiteURL=self.websiteURL, + )