Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

6.1.2 #204

Merged
merged 14 commits into from
Jan 3, 2024
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
lemin_captcha,
rotate_captcha,
datadome_captcha,
friendly_captcha,
cyber_siara_captcha,
draw_around_captcha,
bounding_box_captcha,
Expand Down
3 changes: 3 additions & 0 deletions docs/modules/enum/info.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,6 @@ To import this module:

.. autoenum:: python_rucaptcha.core.enums.GridCaptchaEnm
:members:

.. autoenum:: python_rucaptcha.core.enums.FriendlyCaptchaEnm
:members:
12 changes: 12 additions & 0 deletions docs/modules/friendly-captcha/example.rst
Original file line number Diff line number Diff line change
@@ -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:
2 changes: 1 addition & 1 deletion docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion requirements.style.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# codestyle
isort==5.*
black==23.10.0
black==23.12.0
autoflake==2.*
2 changes: 1 addition & 1 deletion src/python_rucaptcha/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "6.1"
__version__ = "6.1.2"
5 changes: 5 additions & 0 deletions src/python_rucaptcha/core/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,8 @@ class CoordinatesCaptchaEnm(str, MyEnum):

class GridCaptchaEnm(str, MyEnum):
GridTask = "GridTask"


class FriendlyCaptchaEnm(str, MyEnum):
FriendlyCaptchaTaskProxyless = "FriendlyCaptchaTaskProxyless"
FriendlyCaptchaTask = "FriendlyCaptchaTask"
124 changes: 124 additions & 0 deletions src/python_rucaptcha/friendly_captcha.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions src/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ def run(self):
turnstile
amazon
amazon_waf
friendly-captcha
""",
python_requires=REQUIRES_PYTHON,
zip_safe=False,
Expand Down
131 changes: 131 additions & 0 deletions tests/test_friendly_captcha.py
Original file line number Diff line number Diff line change
@@ -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,
)