From ea525b02cda51db2162efa744283fda4cfa87721 Mon Sep 17 00:00:00 2001 From: "pavel.grinkevich" Date: Thu, 30 Jul 2026 19:41:59 +0000 Subject: [PATCH 1/3] pydantic warning fixes, add alibaba, friendly, bugfix, python3.14 support, timeouts fix --- .gitignore | 3 +- .../CapMonsterCloudClient.py | 15 +- capmonstercloud_client/GetResultTimeouts.py | 15 +- capmonstercloud_client/clientOptions.py | 11 +- .../requests/AlibabaCustomTaskRequest.py | 48 +++++++ .../requests/AltchaCustomTaskRequest.py | 5 +- .../requests/CastleCustomTaskRequest.py | 5 +- .../requests/DataDomeCustomTaskRequest.py | 5 +- .../requests/FriendlyCustomTaskRequest.py | 36 +++++ .../requests/FuncaptchaComplexImageTask.py | 11 +- .../requests/GeetestRequest.py | 5 +- .../requests/HcaptchaComplexImageTask.py | 17 ++- .../requests/HuntCustomTaskRequest.py | 5 +- .../requests/ImageToTextRequest.py | 17 ++- .../requests/ImpervaCustomTaskRequest.py | 13 +- .../requests/RecaptchaComplexImageTask.py | 11 +- .../requests/RecaptchaV3EnterpriseRequest.py | 13 +- .../requests/RecaptchaV3ProxylessRequest.py | 5 +- .../RecognitionComplexImageTaskRequest.py | 8 +- .../requests/TemuCustomTaskRequest.py | 5 +- .../requests/TenDiCustomTaskRequest.py | 5 +- .../requests/TspdCustomTaskRequest.py | 5 +- .../requests/TurnstileRequest.py | 5 +- capmonstercloud_client/requests/__init__.py | 6 +- capmonstercloud_client/requests/proxy_info.py | 14 +- capmonstercloud_client/version.txt | 2 +- examples/alibaba.py | 46 +++++++ examples/cf_waitroom.py | 3 +- examples/friendly.py | 45 ++++++ examples/imperva.py | 2 +- examples/recaptchaV3.py | 4 +- examples/turnstile_cf.py | 3 +- requirements.txt | 17 ++- setup.py | 4 + test/alibaba_test.py | 129 ++++++++++++++++++ test/altcha_test.py | 2 +- test/amazonwaf_test.py | 2 +- test/basilisk_test.py | 2 +- test/castle_test.py | 2 +- test/cit_test.py | 2 +- test/cmclient_test.py | 1 + test/datadome_test.py | 2 +- test/fc_image_test.py | 2 +- test/friendly_test.py | 99 ++++++++++++++ test/hc_image_test.py | 2 +- test/imperva_request_test.py | 10 +- test/mtcaptcha_test.py | 2 +- test/prosopo_test.py | 2 +- test/recaptchaV3Enterprise_test.py | 2 +- test/recaptcha_image_test.py | 2 +- test/temu_test.py | 2 +- test/tendi_test.py | 2 +- test/yidun_test.py | 2 +- 53 files changed, 588 insertions(+), 95 deletions(-) create mode 100644 capmonstercloud_client/requests/AlibabaCustomTaskRequest.py create mode 100644 capmonstercloud_client/requests/FriendlyCustomTaskRequest.py create mode 100644 examples/alibaba.py create mode 100644 examples/friendly.py create mode 100644 test/alibaba_test.py create mode 100644 test/friendly_test.py diff --git a/.gitignore b/.gitignore index 62d48de..c2ab60a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ dist/ build/ env/ venv/ -capmonstercloudclient.egg-info/ \ No newline at end of file +capmonstercloudclient.egg-info/ +.mcp.json \ No newline at end of file diff --git a/capmonstercloud_client/CapMonsterCloudClient.py b/capmonstercloud_client/CapMonsterCloudClient.py index 82344cc..54fd867 100644 --- a/capmonstercloud_client/CapMonsterCloudClient.py +++ b/capmonstercloud_client/CapMonsterCloudClient.py @@ -32,7 +32,7 @@ ((BinanceTaskRequest,), getBinanceTimeouts), ((ImpervaCustomTaskRequest,), getImpervaTimeouts), ((RecognitionComplexImageTaskRequest,), getCITTimeouts), - ((MTCaptchaRequest,), getImage2TextTimeouts), + ((MTCaptchaRequest,), getMTCaptchaTimeouts), ((YidunRequest,), getYidunTimeouts), ((TemuCustomTaskRequest,), getTemuTimeouts), ((ProsopoTaskRequest,), getProsopoTimeouts), @@ -40,6 +40,8 @@ ((CastleCustomTaskRequest,), getCastleTimeouts), ((TspdCustomTaskRequest,), getTspdTimeouts), ((HuntCustomTaskRequest,), getHuntTimeouts), + ((AlibabaCustomTaskRequest,), getAlibabaTimeouts), + ((FriendlyCustomTaskRequest,), getFriendlyTimeouts), ) @@ -106,7 +108,9 @@ async def solve_captcha(self, request: Union[ AltchaCustomTaskRequest, CastleCustomTaskRequest, TspdCustomTaskRequest, - HuntCustomTaskRequest], + HuntCustomTaskRequest, + AlibabaCustomTaskRequest, + FriendlyCustomTaskRequest], ) -> Dict[str, str]: ''' Non-blocking method for captcha solving. @@ -145,7 +149,12 @@ async def _solve(self, request: Union[ YidunRequest, TemuCustomTaskRequest, ProsopoTaskRequest, - AltchaCustomTaskRequest], + AltchaCustomTaskRequest, + CastleCustomTaskRequest, + TspdCustomTaskRequest, + HuntCustomTaskRequest, + AlibabaCustomTaskRequest, + FriendlyCustomTaskRequest], timeouts: GetResultTimeouts, ) -> Dict[str, str]: diff --git a/capmonstercloud_client/GetResultTimeouts.py b/capmonstercloud_client/GetResultTimeouts.py index 0b62fef..4d178e0 100644 --- a/capmonstercloud_client/GetResultTimeouts.py +++ b/capmonstercloud_client/GetResultTimeouts.py @@ -36,7 +36,7 @@ def getTurnstileTimeouts() -> GetResultTimeouts: return GetResultTimeouts(1, 0, 1, 80) def getDatadomeTimeouts() -> GetResultTimeouts: - return GetResultTimeouts(1, 0, 1, 80) + return GetResultTimeouts(1, 0, 3, 180) def getTenDiTimeouts() -> GetResultTimeouts: return GetResultTimeouts(1, 10, 3, 180) @@ -51,7 +51,7 @@ def getBinanceTimeouts() -> GetResultTimeouts: return GetResultTimeouts(1, 0, 1, 20) def getImpervaTimeouts() -> GetResultTimeouts: - return GetResultTimeouts(1, 0, 1, 20) + return GetResultTimeouts(1, 0, 1, 15) def getAltchaTimeouts() -> GetResultTimeouts: return GetResultTimeouts(1, 0, 1, 50) @@ -63,11 +63,20 @@ def getTspdTimeouts() -> GetResultTimeouts: return GetResultTimeouts(1, 0, 1, 60) def getHuntTimeouts() -> GetResultTimeouts: - return GetResultTimeouts(1, 0, 1, 60) + return GetResultTimeouts(1, 0, 3, 180) + +def getAlibabaTimeouts() -> GetResultTimeouts: + return GetResultTimeouts(1, 0, 3, 120) + +def getFriendlyTimeouts() -> GetResultTimeouts: + return GetResultTimeouts(1, 0, 3, 180) def getCITTimeouts() -> GetResultTimeouts: return GetResultTimeouts(0.35, 0, 0.2, 10) +def getMTCaptchaTimeouts() -> GetResultTimeouts: + return GetResultTimeouts(1, 10, 3, 180) + def getYidunTimeouts() -> GetResultTimeouts: return GetResultTimeouts(1, 10, 3, 180) diff --git a/capmonstercloud_client/clientOptions.py b/capmonstercloud_client/clientOptions.py index 5d81077..42fef40 100644 --- a/capmonstercloud_client/clientOptions.py +++ b/capmonstercloud_client/clientOptions.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel, validator, Field +from pydantic import BaseModel, field_validator, Field from .requests import ClientProxyInfo from typing import Optional @@ -10,19 +10,22 @@ class ClientOptions(BaseModel): client_timeout: float = Field(default=20.0) - @validator('api_key') + @field_validator('api_key') + @classmethod def validate_api_key(cls, value): if not isinstance(value, str): raise TypeError(f'Api Key must be type, got {type(value)}') return value - @validator('service_url') + @field_validator('service_url') + @classmethod def validate_service_url(cls, value): if not isinstance(value, str): raise TypeError(f'Service url must be type, got {type(value)}') return value - @validator('default_soft_id') + @field_validator('default_soft_id') + @classmethod def validate_soft_id(cls, value): if not isinstance(value, int): raise TypeError(f'Soft id must be type, got {type(value)}') diff --git a/capmonstercloud_client/requests/AlibabaCustomTaskRequest.py b/capmonstercloud_client/requests/AlibabaCustomTaskRequest.py new file mode 100644 index 0000000..18666cf --- /dev/null +++ b/capmonstercloud_client/requests/AlibabaCustomTaskRequest.py @@ -0,0 +1,48 @@ +from typing import Dict, Optional, Union +from pydantic import Field, field_validator + +from .CustomTaskRequestBase import CustomTaskRequestBase + +ALLOWED_METADATA_KEYS = { + 'sceneId', 'prefix', 'userId', 'userUserId', 'verifyType', + 'region', 'UserCertifyId', 'apiGetLib', 'cookieRequired', +} + +class AlibabaCustomTaskRequest(CustomTaskRequestBase): + captchaClass: str = Field(default='alibaba') + metadata: Dict[str, Union[str, bool]] + + @field_validator('metadata') + @classmethod + def validate_metadata(cls, value): + if not set(value.keys()).issubset(ALLOWED_METADATA_KEYS): + raise TypeError(f'Allowed keys for metadata are {sorted(ALLOWED_METADATA_KEYS)}') + if value.get('sceneId') is None: + raise TypeError(f'Expect that sceneId will be defined.') + else: + if not isinstance(value.get('sceneId'), str): + raise TypeError(f'Expect that sceneId will be str.') + if value.get('prefix') is None: + raise TypeError(f'Expect that prefix will be defined.') + else: + if not isinstance(value.get('prefix'), str): + raise TypeError(f'Expect that prefix will be str.') + if value.get('cookieRequired') is not None and not isinstance(value.get('cookieRequired'), bool): + raise TypeError(f'Expect that cookieRequired will be bool.') + return value + + def getTaskDict(self) -> Dict[str, Union[str, int, bool]]: + task = {} + task['type'] = self.type + task['class'] = self.captchaClass + task['websiteURL'] = self.websiteUrl + task['metadata'] = self.metadata + if self.proxy: + task['proxyType'] = self.proxy.proxyType + task['proxyAddress'] = self.proxy.proxyAddress + task['proxyPort'] = self.proxy.proxyPort + task['proxyLogin'] = self.proxy.proxyLogin + task['proxyPassword'] = self.proxy.proxyPassword + if self.userAgent is not None: + task['userAgent'] = self.userAgent + return task diff --git a/capmonstercloud_client/requests/AltchaCustomTaskRequest.py b/capmonstercloud_client/requests/AltchaCustomTaskRequest.py index d7bb9c4..d84d3be 100644 --- a/capmonstercloud_client/requests/AltchaCustomTaskRequest.py +++ b/capmonstercloud_client/requests/AltchaCustomTaskRequest.py @@ -1,5 +1,5 @@ from typing import Dict, Union -from pydantic import Field, validator +from pydantic import Field, field_validator from .CustomTaskRequestBase import CustomTaskRequestBase @@ -8,7 +8,8 @@ class AltchaCustomTaskRequest(CustomTaskRequestBase): websiteKey: str = Field() metadata : Dict[str, str] - @validator('metadata') + @field_validator('metadata') + @classmethod def validate_metadata(cls, value): for key in ['challenge', 'iterations', 'salt', 'signature']: if value.get(key) is None: diff --git a/capmonstercloud_client/requests/CastleCustomTaskRequest.py b/capmonstercloud_client/requests/CastleCustomTaskRequest.py index 2515600..79b555b 100644 --- a/capmonstercloud_client/requests/CastleCustomTaskRequest.py +++ b/capmonstercloud_client/requests/CastleCustomTaskRequest.py @@ -1,5 +1,5 @@ from typing import Dict, Union -from pydantic import Field, validator +from pydantic import Field, field_validator from .CustomTaskRequestBase import CustomTaskRequestBase @@ -8,7 +8,8 @@ class CastleCustomTaskRequest(CustomTaskRequestBase): websiteKey: str = Field() metadata: Dict[str, Union[str, int]] - @validator('metadata') + @field_validator('metadata') + @classmethod def validate_metadata(cls, value): if value.get('wUrl') is None: raise TypeError(f'Expected that wUrl is defined.') diff --git a/capmonstercloud_client/requests/DataDomeCustomTaskRequest.py b/capmonstercloud_client/requests/DataDomeCustomTaskRequest.py index 179cd67..d097786 100644 --- a/capmonstercloud_client/requests/DataDomeCustomTaskRequest.py +++ b/capmonstercloud_client/requests/DataDomeCustomTaskRequest.py @@ -1,12 +1,13 @@ from typing import Dict, Union -from pydantic import Field, validator, model_validator +from pydantic import Field, field_validator, model_validator from .CustomTaskRequestBase import CustomTaskRequestBase class DataDomeCustomTaskRequest(CustomTaskRequestBase): captchaClass: str = Field(default='DataDome') metadata : Dict[str, str] - @validator('metadata') + @field_validator('metadata') + @classmethod def validate_metadata(cls, value): if value.get('datadomeCookie') is None: raise TypeError(f'Expect that datadomeCookie will be defined.') diff --git a/capmonstercloud_client/requests/FriendlyCustomTaskRequest.py b/capmonstercloud_client/requests/FriendlyCustomTaskRequest.py new file mode 100644 index 0000000..b8e0756 --- /dev/null +++ b/capmonstercloud_client/requests/FriendlyCustomTaskRequest.py @@ -0,0 +1,36 @@ +from typing import Dict, Union +from pydantic import Field, field_validator + +from .CustomTaskRequestBase import CustomTaskRequestBase + +class FriendlyCustomTaskRequest(CustomTaskRequestBase): + captchaClass: str = Field(default='friendly') + websiteKey: str = Field() + metadata: Dict[str, str] + + @field_validator('metadata') + @classmethod + def validate_metadata(cls, value): + if value.get('apiGetLib') is None: + raise TypeError(f'Expect that apiGetLib will be defined.') + else: + if not isinstance(value.get('apiGetLib'), str): + raise TypeError(f'Expect that apiGetLib will be str.') + return value + + def getTaskDict(self) -> Dict[str, Union[str, int, bool]]: + task = {} + task['type'] = self.type + task['class'] = self.captchaClass + task['websiteURL'] = self.websiteUrl + task['websiteKey'] = self.websiteKey + task['metadata'] = self.metadata + if self.proxy: + task['proxyType'] = self.proxy.proxyType + task['proxyAddress'] = self.proxy.proxyAddress + task['proxyPort'] = self.proxy.proxyPort + task['proxyLogin'] = self.proxy.proxyLogin + task['proxyPassword'] = self.proxy.proxyPassword + if self.userAgent is not None: + task['userAgent'] = self.userAgent + return task diff --git a/capmonstercloud_client/requests/FuncaptchaComplexImageTask.py b/capmonstercloud_client/requests/FuncaptchaComplexImageTask.py index 22f464b..dcb1341 100644 --- a/capmonstercloud_client/requests/FuncaptchaComplexImageTask.py +++ b/capmonstercloud_client/requests/FuncaptchaComplexImageTask.py @@ -1,5 +1,5 @@ from typing import Dict, Union -from pydantic import Field, validator +from pydantic import Field, field_validator from .ComplexImageTaskBase import ComplexImageTaskRequestBase from ..exceptions import NumbersImagesErrors, ZeroImagesErrors, TaskNotDefinedError @@ -9,14 +9,16 @@ class FunCaptchaComplexImageTaskRequest(ComplexImageTaskRequestBase): captchaClass: str = Field(default='funcaptcha') metadata : Dict[str, str] - @validator('metadata') + @field_validator('metadata') + @classmethod def validate_metadata(cls, value): if value.get('Task') is None: raise TaskNotDefinedError(f'Expect that task will be defined.') else: return value - @validator('imagesUrls') + @field_validator('imagesUrls') + @classmethod def validate_urls_array(cls, value): if value is not None: if not isinstance(value, (list, tuple)): @@ -31,7 +33,8 @@ def validate_urls_array(cls, value): raise TypeError(f'Next images from imagesUrls array are not string: {contain_types}') return value - @validator('imagesBase64') + @field_validator('imagesBase64') + @classmethod def validate_images_array(cls, value): if value is not None: if not isinstance(value, (list, tuple)): diff --git a/capmonstercloud_client/requests/GeetestRequest.py b/capmonstercloud_client/requests/GeetestRequest.py index e3bcf1e..b4bbf87 100644 --- a/capmonstercloud_client/requests/GeetestRequest.py +++ b/capmonstercloud_client/requests/GeetestRequest.py @@ -1,4 +1,4 @@ -from pydantic import Field, validator +from pydantic import Field, field_validator from typing import Dict, Union, Optional from .baseRequestWithProxy import BaseRequestWithProxy @@ -15,7 +15,8 @@ class GeetestRequest(BaseRequestWithProxy): geetestGetLib: Optional[str] = Field(default=None) user_agent: Optional[str] = Field(default=None) - @validator('version') + @field_validator('version') + @classmethod def validate_version(cls, value): if value not in [3, 4]: raise ValueError(f"Geetest version could be 3 or 4, not {value}") diff --git a/capmonstercloud_client/requests/HcaptchaComplexImageTask.py b/capmonstercloud_client/requests/HcaptchaComplexImageTask.py index 06fe427..3c8f88b 100644 --- a/capmonstercloud_client/requests/HcaptchaComplexImageTask.py +++ b/capmonstercloud_client/requests/HcaptchaComplexImageTask.py @@ -1,5 +1,5 @@ from typing import Dict, Union, List, Optional -from pydantic import Field, validator +from pydantic import Field, field_validator from .ComplexImageTaskBase import ComplexImageTaskRequestBase from ..exceptions import NumbersImagesErrors, ZeroImagesErrors, TaskNotDefinedError, ExtraParamsError @@ -36,26 +36,31 @@ def _validate_image_array(value, field_name, max_images): raise TypeError(f'Next images from imagesUrls array are not string: {contain_types}') return value - @validator('metadata') + @field_validator('metadata') + @classmethod def validate_metadata(cls, value): if value.get('Task') is None: raise TaskNotDefinedError('Expect that task will be defined.') else: return value - @validator('exampleImageUrls') + @field_validator('exampleImageUrls') + @classmethod def validate_example_image_urls(cls, value): return cls._validate_image_array(value, 'exampleImageUrls', 1) - @validator('exampleImagesBase64') + @field_validator('exampleImagesBase64') + @classmethod def validate_example_images_base64(cls, value): return cls._validate_image_array(value, 'exampleImagesBase64', 1) - @validator('imagesUrls') + @field_validator('imagesUrls') + @classmethod def validate_images_urls(cls, value): return cls._validate_image_array(value, 'imagesUrls', 18) - @validator('imagesBase64') + @field_validator('imagesBase64') + @classmethod def validate_images_base64(cls, value): return cls._validate_image_array(value, 'imagesBase64', 18) diff --git a/capmonstercloud_client/requests/HuntCustomTaskRequest.py b/capmonstercloud_client/requests/HuntCustomTaskRequest.py index 0dd1302..ba08143 100644 --- a/capmonstercloud_client/requests/HuntCustomTaskRequest.py +++ b/capmonstercloud_client/requests/HuntCustomTaskRequest.py @@ -1,5 +1,5 @@ from typing import Dict, Union -from pydantic import Field, validator, model_validator +from pydantic import Field, field_validator, model_validator from .CustomTaskRequestBase import CustomTaskRequestBase @@ -7,7 +7,8 @@ class HuntCustomTaskRequest(CustomTaskRequestBase): captchaClass: str = Field(default='HUNT') metadata: Dict[str, str] - @validator('metadata') + @field_validator('metadata') + @classmethod def validate_metadata(cls, value): if value.get('apiGetLib') is None: raise TypeError(f'Expect that apiGetLib will be defined.') diff --git a/capmonstercloud_client/requests/ImageToTextRequest.py b/capmonstercloud_client/requests/ImageToTextRequest.py index 6755a82..4a3b468 100644 --- a/capmonstercloud_client/requests/ImageToTextRequest.py +++ b/capmonstercloud_client/requests/ImageToTextRequest.py @@ -1,6 +1,6 @@ import base64 -from pydantic import validator, Field +from pydantic import field_validator, Field from typing import Optional, Dict, Union from .baseRequest import BaseRequest from .enums import TextModules @@ -15,14 +15,16 @@ class ImageToTextRequest(BaseRequest): numeric: Optional[int] = Field(default=None) math: Optional[bool] = Field(default=None) - @validator('threshold') + @field_validator('threshold') + @classmethod def validate_threshold(cls, value): if value is not None: if value not in range(0, 101): raise ValueError(f"threshold must be between 1 and 100, got {value}") return value - @validator('module_name') + @field_validator('module_name') + @classmethod def validate_module_name(cls, value): if value is not None: if value not in TextModules.list_values(): @@ -30,21 +32,24 @@ def validate_module_name(cls, value): f"got '{value}'") return value - @validator('case') + @field_validator('case') + @classmethod def validate_case_type(cls, value): if value is not None: if not isinstance(value, bool): raise TypeError(f'Case value must be type as boolean, not {type(value)}') return value - @validator('numeric') + @field_validator('numeric') + @classmethod def validate_numeric_range(cls, value): if value is not None: if not value in range(0, 2): raise ValueError(f'numeric must be between [0, 1], got {value}') return value - @validator('math') + @field_validator('math') + @classmethod def validate_math_type(cls, value): if value is not None: if not isinstance(value, bool): diff --git a/capmonstercloud_client/requests/ImpervaCustomTaskRequest.py b/capmonstercloud_client/requests/ImpervaCustomTaskRequest.py index 2b7ed8b..86f746d 100644 --- a/capmonstercloud_client/requests/ImpervaCustomTaskRequest.py +++ b/capmonstercloud_client/requests/ImpervaCustomTaskRequest.py @@ -1,5 +1,5 @@ from typing import Dict, Union -from pydantic import Field, validator, model_validator +from pydantic import Field, field_validator, model_validator from .CustomTaskRequestBase import CustomTaskRequestBase @@ -7,18 +7,19 @@ class ImpervaCustomTaskRequest(CustomTaskRequestBase): captchaClass: str = Field(default='Imperva') metadata : Dict[str, str] - @validator('metadata') + @field_validator('metadata') + @classmethod def validate_metadata(cls, value): if value.get('incapsulaScriptUrl') is None: raise TypeError(f'Expect that incapsulaScriptUrl will be defined.') else: if not isinstance(value.get('incapsulaScriptUrl'), str): raise TypeError(f'Expect that incapsulaScriptUrl will be str.') - if value.get('incapsulaCookie') is None: - raise TypeError(f'Expect that incapsulaCookie will be defined.') + if value.get('incapsulaCookies') is None: + raise TypeError(f'Expect that incapsulaCookies will be defined.') else: - if not isinstance(value.get('incapsulaCookie'), str): - raise TypeError(f'Expect that incapsulaCookie will be str.') + if not isinstance(value.get('incapsulaCookies'), str): + raise TypeError(f'Expect that incapsulaCookies will be str.') if value.get('reese84UrlEndpoint') is not None and not isinstance(value.get('reese84UrlEndpoint'), str): raise TypeError(f'Expect that reese84UrlEndpoint will be str.') return value diff --git a/capmonstercloud_client/requests/RecaptchaComplexImageTask.py b/capmonstercloud_client/requests/RecaptchaComplexImageTask.py index bdadac9..0b7e878 100644 --- a/capmonstercloud_client/requests/RecaptchaComplexImageTask.py +++ b/capmonstercloud_client/requests/RecaptchaComplexImageTask.py @@ -1,5 +1,5 @@ from typing import Dict, Union -from pydantic import Field, validator +from pydantic import Field, field_validator from .ComplexImageTaskBase import ComplexImageTaskRequestBase from ..exceptions import NumbersImagesErrors, ZeroImagesErrors, TaskNotDefinedError @@ -9,7 +9,8 @@ class RecaptchaComplexImageTaskRequest(ComplexImageTaskRequestBase): metadata : Dict[str, str] captchaClass: str = Field(default='recaptcha') - @validator('metadata') + @field_validator('metadata') + @classmethod def validate_metadata(cls, value): if value.get('Task') is None and value.get('TaskDefinition') is None: raise TaskNotDefinedError(f'Expect at least one of value(Task or TaskDefinition) will be filled.') @@ -18,7 +19,8 @@ def validate_metadata(cls, value): else: return value - @validator('imagesUrls') + @field_validator('imagesUrls') + @classmethod def validate_urls_array(cls, value): if value is not None: if not isinstance(value, (list, tuple)): @@ -33,7 +35,8 @@ def validate_urls_array(cls, value): raise TypeError(f'Next images from imagesUrls array are not string: {contain_types}') return value - @validator('imagesBase64') + @field_validator('imagesBase64') + @classmethod def validate_images_array(cls, value): if value is not None: if not isinstance(value, (list, tuple)): diff --git a/capmonstercloud_client/requests/RecaptchaV3EnterpriseRequest.py b/capmonstercloud_client/requests/RecaptchaV3EnterpriseRequest.py index 32625c5..3621abf 100644 --- a/capmonstercloud_client/requests/RecaptchaV3EnterpriseRequest.py +++ b/capmonstercloud_client/requests/RecaptchaV3EnterpriseRequest.py @@ -1,5 +1,5 @@ from typing import Dict, Union, Optional -from pydantic import Field +from pydantic import Field, field_validator from .baseRequest import BaseRequest @@ -9,7 +9,16 @@ class RecaptchaV3EnterpriseRequest(BaseRequest): websiteKey: str minScore: Optional[float] = Field(default=None) pageAction: Optional[str] = Field(default=None) - + + @field_validator('minScore') + @classmethod + def validate_min_score(cls, value): + if value is not None: + if not 0.1 <= value <= 0.9: + raise ValueError(f'Minimum score value should be found in interval 0.1 - 0.9, ' \ + f'current "{value}".') + return value + def getTaskDict(self) -> Dict[str, Union[str, int, float]]: task = {} task['type'] = self.type diff --git a/capmonstercloud_client/requests/RecaptchaV3ProxylessRequest.py b/capmonstercloud_client/requests/RecaptchaV3ProxylessRequest.py index 3865f09..2c564d0 100644 --- a/capmonstercloud_client/requests/RecaptchaV3ProxylessRequest.py +++ b/capmonstercloud_client/requests/RecaptchaV3ProxylessRequest.py @@ -1,5 +1,5 @@ from typing import Optional, Union, Dict -from pydantic import Field, validator +from pydantic import Field, field_validator from .baseRequest import BaseRequest @@ -10,7 +10,8 @@ class RecaptchaV3ProxylessRequest(BaseRequest): minScore: Optional[float] = Field(default=None) pageAction: Optional[str] = Field(default=None) - @validator('minScore') + @field_validator('minScore') + @classmethod def validate_min_score(cls, value): if value is not None: if not 0.1 <= value <= 0.9: diff --git a/capmonstercloud_client/requests/RecognitionComplexImageTaskRequest.py b/capmonstercloud_client/requests/RecognitionComplexImageTaskRequest.py index dec102b..ea9b865 100644 --- a/capmonstercloud_client/requests/RecognitionComplexImageTaskRequest.py +++ b/capmonstercloud_client/requests/RecognitionComplexImageTaskRequest.py @@ -1,5 +1,5 @@ from typing import Dict, Union -from pydantic import validator +from pydantic import field_validator from .ComplexImageTaskBase import ComplexImageTaskRequestBase from ..exceptions import ZeroImagesErrors, TaskNotDefinedError @@ -8,7 +8,8 @@ class RecognitionComplexImageTaskRequest(ComplexImageTaskRequestBase): captchaClass: str = 'recognition' metadata: Dict[str, str] - @validator('metadata') + @field_validator('metadata') + @classmethod def validate_metadata(cls, value): if value.get('Task') is None: raise TaskNotDefinedError(f'Expect that Task will be defined.') @@ -19,7 +20,8 @@ def validate_metadata(cls, value): raise TypeError(f'Allowed keys for metadata are "Task" and "TaskArgument"') return value - @validator('imagesBase64') + @field_validator('imagesBase64') + @classmethod def validate_images_array(cls, value): if value is not None: if not isinstance(value, (list, tuple)): diff --git a/capmonstercloud_client/requests/TemuCustomTaskRequest.py b/capmonstercloud_client/requests/TemuCustomTaskRequest.py index 2a9820f..8b5ed7b 100644 --- a/capmonstercloud_client/requests/TemuCustomTaskRequest.py +++ b/capmonstercloud_client/requests/TemuCustomTaskRequest.py @@ -1,5 +1,5 @@ from typing import Dict, Union -from pydantic import Field, validator +from pydantic import Field, field_validator from .CustomTaskRequestBase import CustomTaskRequestBase @@ -7,7 +7,8 @@ class TemuCustomTaskRequest(CustomTaskRequestBase): captchaClass: str = Field(default='Temu') metadata: Dict[str, str] - @validator('metadata') + @field_validator('metadata') + @classmethod def validate_metadata(cls, value): if value.get('cookie') is None: raise TypeError(f'Expect that cookie will be defined.') diff --git a/capmonstercloud_client/requests/TenDiCustomTaskRequest.py b/capmonstercloud_client/requests/TenDiCustomTaskRequest.py index 0b6dc63..ca0bed5 100644 --- a/capmonstercloud_client/requests/TenDiCustomTaskRequest.py +++ b/capmonstercloud_client/requests/TenDiCustomTaskRequest.py @@ -1,5 +1,5 @@ from typing import Dict, Optional, Union -from pydantic import Field, validator +from pydantic import Field, field_validator from .CustomTaskRequestBase import CustomTaskRequestBase @@ -8,7 +8,8 @@ class TenDiCustomTaskRequest(CustomTaskRequestBase): websiteKey: str = Field() metadata: Optional[Dict[str, str]] = Field(default=None) - @validator('metadata') + @field_validator('metadata') + @classmethod def validate_metadata(cls, value): if value is not None: if not set(value.keys()).issubset(set(["captchaUrl"])): diff --git a/capmonstercloud_client/requests/TspdCustomTaskRequest.py b/capmonstercloud_client/requests/TspdCustomTaskRequest.py index c5781d9..4018f4b 100644 --- a/capmonstercloud_client/requests/TspdCustomTaskRequest.py +++ b/capmonstercloud_client/requests/TspdCustomTaskRequest.py @@ -1,5 +1,5 @@ from typing import Dict, Union -from pydantic import Field, validator, model_validator +from pydantic import Field, field_validator, model_validator from .CustomTaskRequestBase import CustomTaskRequestBase @@ -8,7 +8,8 @@ class TspdCustomTaskRequest(CustomTaskRequestBase): userAgent: str = Field() metadata: Dict[str, str] - @validator('metadata') + @field_validator('metadata') + @classmethod def validate_metadata(cls, value): if value.get('tspdCookie') is None: raise TypeError(f'Expect that tspdCookie will be defined.') diff --git a/capmonstercloud_client/requests/TurnstileRequest.py b/capmonstercloud_client/requests/TurnstileRequest.py index 4bc0776..3851961 100644 --- a/capmonstercloud_client/requests/TurnstileRequest.py +++ b/capmonstercloud_client/requests/TurnstileRequest.py @@ -1,5 +1,5 @@ from typing import Dict, Optional, Union -from pydantic import Field, validator, model_validator +from pydantic import Field, field_validator, model_validator from .baseRequestWithProxy import BaseRequestWithProxy @@ -16,7 +16,8 @@ class TurnstileRequest(BaseRequestWithProxy): htmlPageBase64: Optional[str] = Field(default=None) apiJsUrl: Optional[str] = Field(default=None) - @validator('cloudflareTaskType') + @field_validator('cloudflareTaskType') + @classmethod def validate_cloudflare_task(cls, value): if value is not None: if value not in ['cf_clearance', 'token', 'wait_room']: diff --git a/capmonstercloud_client/requests/__init__.py b/capmonstercloud_client/requests/__init__.py index 4328baa..86a651c 100644 --- a/capmonstercloud_client/requests/__init__.py +++ b/capmonstercloud_client/requests/__init__.py @@ -27,6 +27,8 @@ from .CastleCustomTaskRequest import CastleCustomTaskRequest from .TspdCustomTaskRequest import TspdCustomTaskRequest from .HuntCustomTaskRequest import HuntCustomTaskRequest +from .AlibabaCustomTaskRequest import AlibabaCustomTaskRequest +from .FriendlyCustomTaskRequest import FriendlyCustomTaskRequest from .proxy_info import ProxyInfo, ClientProxyInfo @@ -50,5 +52,7 @@ 'AltchaCustomTaskRequest', 'CastleCustomTaskRequest', 'TspdCustomTaskRequest', - 'HuntCustomTaskRequest' + 'HuntCustomTaskRequest', + 'AlibabaCustomTaskRequest', + 'FriendlyCustomTaskRequest' ] diff --git a/capmonstercloud_client/requests/proxy_info.py b/capmonstercloud_client/requests/proxy_info.py index 3811d34..9d89d13 100644 --- a/capmonstercloud_client/requests/proxy_info.py +++ b/capmonstercloud_client/requests/proxy_info.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel, validator +from pydantic import BaseModel, field_validator from .enums import ProxyTypes from typing import Optional @@ -9,13 +9,15 @@ class ProxyInfo(BaseModel): proxyLogin: str proxyPassword: str - @validator('proxyType') + @field_validator('proxyType') + @classmethod def validate_proxy_type(cls, value): if value not in ProxyTypes.list_values(): raise ValueError(f'Expected that proxy type will be in {ProxyTypes.list_values()}, got "{value}"') return value - @validator('proxyPort') + @field_validator('proxyPort') + @classmethod def validate_port(cls, value): if not isinstance(value, int): raise TypeError(f'Expect that port value will be type, got {type(value)}') @@ -28,13 +30,15 @@ class ClientProxyInfo(BaseModel): proxyLogin: Optional[str] = None proxyPassword: Optional[str] = None - @validator('proxyType') + @field_validator('proxyType') + @classmethod def validate_proxy_type(cls, value): if value not in ProxyTypes.list_values(): raise ValueError(f'Expected that proxy type will be in {ProxyTypes.list_values()}, got "{value}"') return value - @validator('proxyPort') + @field_validator('proxyPort') + @classmethod def validate_port(cls, value): if not isinstance(value, int): raise TypeError(f'Expect that port value will be type, got {type(value)}') diff --git a/capmonstercloud_client/version.txt b/capmonstercloud_client/version.txt index 0c89fc9..ef8d756 100644 --- a/capmonstercloud_client/version.txt +++ b/capmonstercloud_client/version.txt @@ -1 +1 @@ -4.0.0 \ No newline at end of file +4.2.0 \ No newline at end of file diff --git a/examples/alibaba.py b/examples/alibaba.py new file mode 100644 index 0000000..6f0938d --- /dev/null +++ b/examples/alibaba.py @@ -0,0 +1,46 @@ +import os +import time +import asyncio + +from capmonstercloudclient.requests import AlibabaCustomTaskRequest +from capmonstercloudclient import ClientOptions, CapMonsterClient + + +async def solve_captcha_sync(num_requests): + return [await cap_monster_client.solve_captcha(alibaba_request) for _ in range(num_requests)] + + +async def solve_captcha_async(num_requests): + tasks = [asyncio.create_task(cap_monster_client.solve_captcha(alibaba_request)) + for _ in range(num_requests)] + return await asyncio.gather(*tasks, return_exceptions=True) + + +if __name__ == '__main__': + key = os.getenv('API_KEY') + client_options = ClientOptions(api_key=key) + cap_monster_client = CapMonsterClient(options=client_options) + + metadata = { + "sceneId": "1ww7426c", + "prefix": "dlw3kug", + "apiGetLib": "https://o.example.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js?t=2041", + } + alibaba_request = AlibabaCustomTaskRequest( + websiteUrl='https://www.example.com', + metadata=metadata, + userAgent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', + ) + + nums = 3 + # Sync test + sync_start = time.time() + sync_responses = asyncio.run(solve_captcha_sync(nums)) + print(f'average execution time sync {1/((time.time()-sync_start)/nums):0.2f} ' \ + f'resp/sec\nsolution: {sync_responses[0]}') + + # Async test + async_start = time.time() + async_responses = asyncio.run(solve_captcha_async(nums)) + print(f'average execution time async {1/((time.time()-async_start)/nums):0.2f} ' \ + f'resp/sec\nsolution: {async_responses[0]}') diff --git a/examples/cf_waitroom.py b/examples/cf_waitroom.py index b92ee7a..4752599 100644 --- a/examples/cf_waitroom.py +++ b/examples/cf_waitroom.py @@ -1,3 +1,4 @@ +import os import asyncio from capmonstercloudclient import CapMonsterClient, ClientOptions from capmonstercloudclient.requests import TurnstileRequest @@ -11,7 +12,7 @@ async def main(): proxyLogin='login', # get at https://docs.zennolab.com/zennoproxy/introduction or your own proxy proxyPassword='password' # get at https://docs.zennolab.com/zennoproxy/introduction or your own proxy ) - client_options = ClientOptions(api_key="dac3599143bdfd88dfc41758c6cb8729", client_proxy=client_proxy) + client_options = ClientOptions(api_key=os.getenv("API_KEY"), client_proxy=client_proxy) cap_monster_client = CapMonsterClient(options=client_options) proxy = ProxyInfo( diff --git a/examples/friendly.py b/examples/friendly.py new file mode 100644 index 0000000..685cf0f --- /dev/null +++ b/examples/friendly.py @@ -0,0 +1,45 @@ +import os +import time +import asyncio + +from capmonstercloudclient.requests import FriendlyCustomTaskRequest +from capmonstercloudclient import ClientOptions, CapMonsterClient + + +async def solve_captcha_sync(num_requests): + return [await cap_monster_client.solve_captcha(friendly_request) for _ in range(num_requests)] + + +async def solve_captcha_async(num_requests): + tasks = [asyncio.create_task(cap_monster_client.solve_captcha(friendly_request)) + for _ in range(num_requests)] + return await asyncio.gather(*tasks, return_exceptions=True) + + +if __name__ == '__main__': + key = os.getenv('API_KEY') + client_options = ClientOptions(api_key=key) + cap_monster_client = CapMonsterClient(options=client_options) + + metadata = { + "apiGetLib": "https://cdn.jsdelivr.net/npm/friendly-challenge@0.9.18/widget.module.min.js", + } + friendly_request = FriendlyCustomTaskRequest( + websiteUrl='https://www.example.com', + websiteKey='FCMSITEKEY000000000000000000000000000000', + metadata=metadata, + userAgent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', + ) + + nums = 3 + # Sync test + sync_start = time.time() + sync_responses = asyncio.run(solve_captcha_sync(nums)) + print(f'average execution time sync {1/((time.time()-sync_start)/nums):0.2f} ' \ + f'resp/sec\nsolution: {sync_responses[0]}') + + # Async test + async_start = time.time() + async_responses = asyncio.run(solve_captcha_async(nums)) + print(f'average execution time async {1/((time.time()-async_start)/nums):0.2f} ' \ + f'resp/sec\nsolution: {async_responses[0]}') diff --git a/examples/imperva.py b/examples/imperva.py index cf87af4..b39cfac 100644 --- a/examples/imperva.py +++ b/examples/imperva.py @@ -27,7 +27,7 @@ async def solve_captcha_async(num_requests): proxyPassword="proxyPasswordHere" ) metadata = {"incapsulaScriptUrl": "_Incapsula_Resource?SWJIYLWA=719d34d31c8e3a6e6fffd425f7e032f3", - "incapsulaCookie": "incap_ses_1166_2930313=br7iX33ZNCtf3HlpEXcuEDzz72cAAAAA0suDnBGrq/iA0J4oERYzjQ==; visid_incap_2930313=P3hgPVm9S8Oond1L0sXhZqfK72cAAAAAQUIPAAAAAABoMSY9xZ34RvRseJRiY6s+;", + "incapsulaCookies": "incap_ses_1166_2930313=br7iX33ZNCtf3HlpEXcuEDzz72cAAAAA0suDnBGrq/iA0J4oERYzjQ==; visid_incap_2930313=P3hgPVm9S8Oond1L0sXhZqfK72cAAAAAQUIPAAAAAABoMSY9xZ34RvRseJRiY6s+;", "reese84UrlEndpoint": "Built-with-the-For-hopence-Hurleysurfecting-the-"} imperva_request = ImpervaCustomTaskRequest( websiteUrl='https://example.com/login', diff --git a/examples/recaptchaV3.py b/examples/recaptchaV3.py index 7e236ab..75805c0 100644 --- a/examples/recaptchaV3.py +++ b/examples/recaptchaV3.py @@ -3,7 +3,7 @@ import asyncio from capmonstercloudclient import CapMonsterClient, ClientOptions -from capmonstercloudclient.requests import RecaptchaV3Request +from capmonstercloudclient.requests import RecaptchaV3ProxylessRequest async def solve_captcha_sync(num_requests): return [await cap_monster_client.solve_captcha(recaptcha3request) for _ in range(num_requests)] @@ -19,7 +19,7 @@ async def solve_captcha_async(num_requests): client_options = ClientOptions(api_key=key) cap_monster_client = CapMonsterClient(options=client_options) - recaptcha3request = RecaptchaV3Request(websiteUrl="https://lessons.zennolab.com/captchas/recaptcha/v3.php?level=beta", + recaptcha3request = RecaptchaV3ProxylessRequest(websiteUrl="https://lessons.zennolab.com/captchas/recaptcha/v3.php?level=beta", websiteKey="6Le0xVgUAAAAAIt20XEB4rVhYOODgTl00d8juDob", minScore=0.9) diff --git a/examples/turnstile_cf.py b/examples/turnstile_cf.py index d24df7c..9e915d0 100644 --- a/examples/turnstile_cf.py +++ b/examples/turnstile_cf.py @@ -1,10 +1,11 @@ +import os import asyncio from capmonstercloudclient import CapMonsterClient, ClientOptions from capmonstercloudclient.requests import TurnstileRequest from capmonstercloudclient.requests.baseRequestWithProxy import ProxyInfo async def main(): - client_options = ClientOptions(api_key="dac3599143bdfd88dfc41758c6cb8729") + client_options = ClientOptions(api_key=os.getenv("API_KEY")) cap_monster_client = CapMonsterClient(options=client_options) proxy = ProxyInfo( diff --git a/requirements.txt b/requirements.txt index 51df935..d37af0d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,4 +15,19 @@ yarl==1.18.3 ; python_version >= "3.9" and python_version <= "3.13" aiohttp==3.7.4 ; python_version >= "3.6" and python_version <= "3.8" pydantic==2.10.3 ; python_version >= "3.8" and python_version < "3.9" pydantic==2.5.3 ; python_version >= "3.7" and python_version < "3.8" -pydantic==2.1.* ; python_version < "3.7" \ No newline at end of file +pydantic==2.1.* ; python_version < "3.7" + +aiohappyeyeballs==2.7.1 ; python_version == "3.14" +aiohttp==3.14.3 ; python_version == "3.14" +aiosignal==1.4.0 ; python_version == "3.14" +annotated-types==0.8.0 ; python_version == "3.14" +attrs==26.1.0 ; python_version == "3.14" +frozenlist==1.8.0 ; python_version == "3.14" +idna==3.18 ; python_version == "3.14" +multidict==6.7.1 ; python_version == "3.14" +propcache==0.5.2 ; python_version == "3.14" +pydantic-core==2.46.4 ; python_version == "3.14" +pydantic==2.13.4 ; python_version == "3.14" +typing-extensions==4.16.0 ; python_version == "3.14" +typing-inspection==0.4.2 ; python_version == "3.14" +yarl==1.24.5 ; python_version == "3.14" \ No newline at end of file diff --git a/setup.py b/setup.py index ba5151d..8318049 100644 --- a/setup.py +++ b/setup.py @@ -62,6 +62,10 @@ "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", "Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Operating System :: Unix", diff --git a/test/alibaba_test.py b/test/alibaba_test.py new file mode 100644 index 0000000..c0dd6cb --- /dev/null +++ b/test/alibaba_test.py @@ -0,0 +1,129 @@ +import unittest + +from pydantic import ValidationError +from capmonstercloudclient.requests import AlibabaCustomTaskRequest, ProxyInfo + + +class AlibabaCustomTaskRequestTest(unittest.TestCase): + websiteUrlExample = "https://example.com" + userAgentExample = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36" + sceneIdExample = "1ww7426c" + prefixExample = "dlw3kug" + + def setUp(self): + self.proxy = ProxyInfo( + proxyType="http", + proxyAddress="8.8.8.8", + proxyPort=8080, + proxyLogin="proxyLoginHere", + proxyPassword="proxyPasswordHere" + ) + + def test_alibaba_request_required_fields(self): + required_fields = ["type", "class", "websiteURL", "metadata"] + metadata_required_fields = ["sceneId", "prefix"] + metadata_example = { + "sceneId": self.sceneIdExample, + "prefix": self.prefixExample, + } + request = AlibabaCustomTaskRequest( + websiteUrl=self.websiteUrlExample, + metadata=metadata_example, + ) + task_dictionary = request.getTaskDict() + for f in required_fields: + self.assertTrue( + f in list(task_dictionary.keys()), + msg=f'Required captcha input key "{f}" does not include to request.', + ) + for f in metadata_required_fields: + self.assertTrue( + f in list(task_dictionary["metadata"].keys()), + msg=f'Required captcha input key "{f}" does not include to request.', + ) + self.assertEqual(task_dictionary["class"], "alibaba") + self.assertEqual(task_dictionary["type"], "CustomTask") + + def test_alibaba_metadata_validation(self): + base_kwargs = { + "websiteUrl": self.websiteUrlExample, + "metadata": {}, + } + self.assertRaises(TypeError, AlibabaCustomTaskRequest, **base_kwargs) + base_kwargs["metadata"]["sceneId"] = self.sceneIdExample + self.assertRaises(TypeError, AlibabaCustomTaskRequest, **base_kwargs) + base_kwargs["metadata"]["prefix"] = self.prefixExample + AlibabaCustomTaskRequest(**base_kwargs) + + def test_alibaba_metadata_invalid_key(self): + base_kwargs = { + "websiteUrl": self.websiteUrlExample, + "metadata": { + "sceneId": self.sceneIdExample, + "prefix": self.prefixExample, + "unknownField": "value", + }, + } + self.assertRaises(TypeError, AlibabaCustomTaskRequest, **base_kwargs) + + def test_alibaba_metadata_extended_fields(self): + metadata_example = { + "sceneId": self.sceneIdExample, + "prefix": self.prefixExample, + "userId": "HpadJlQnz2zSKcSmjXBaqQvjYUvP4jMJIk/ZwGNDNiM=", + "userUserId": "/uSXKkVFuuwxXA21/MpXGxpLStWBEup1B3jjlMUWwNE=", + "verifyType": "1.0", + "region": "sgp", + "UserCertifyId": "0a03e59417757735511105780e2a5e", + "apiGetLib": "https://o.example.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js?t=2041", + "cookieRequired": True, + } + request = AlibabaCustomTaskRequest( + websiteUrl=self.websiteUrlExample, + metadata=metadata_example, + ) + task_dictionary = request.getTaskDict() + self.assertEqual(task_dictionary["metadata"], metadata_example) + + def test_alibaba_missing_fields(self): + metadata_example = { + "sceneId": self.sceneIdExample, + "prefix": self.prefixExample, + } + base_kwargs = {} + self.assertRaises(ValidationError, AlibabaCustomTaskRequest, **base_kwargs) + base_kwargs.update({"websiteUrl": self.websiteUrlExample}) + self.assertRaises(ValidationError, AlibabaCustomTaskRequest, **base_kwargs) + base_kwargs.update({"metadata": metadata_example}) + AlibabaCustomTaskRequest(**base_kwargs) + + def test_alibaba_optional_proxy_and_useragent(self): + metadata_example = { + "sceneId": self.sceneIdExample, + "prefix": self.prefixExample, + } + request = AlibabaCustomTaskRequest( + websiteUrl=self.websiteUrlExample, + metadata=metadata_example, + proxy=self.proxy, + userAgent=self.userAgentExample, + ) + task_dictionary = request.getTaskDict() + self.assertEqual(task_dictionary["proxyType"], self.proxy.proxyType) + self.assertEqual(task_dictionary["userAgent"], self.userAgentExample) + + def test_alibaba_proxy_not_required(self): + metadata_example = { + "sceneId": self.sceneIdExample, + "prefix": self.prefixExample, + } + request = AlibabaCustomTaskRequest( + websiteUrl=self.websiteUrlExample, + metadata=metadata_example, + ) + task_dictionary = request.getTaskDict() + self.assertNotIn("proxyType", task_dictionary) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/altcha_test.py b/test/altcha_test.py index 728b89a..4622c1e 100644 --- a/test/altcha_test.py +++ b/test/altcha_test.py @@ -1,6 +1,6 @@ import unittest -from pydantic.error_wrappers import ValidationError +from pydantic import ValidationError from capmonstercloudclient.requests import AltchaCustomTaskRequest diff --git a/test/amazonwaf_test.py b/test/amazonwaf_test.py index 60cfc60..d7140d2 100644 --- a/test/amazonwaf_test.py +++ b/test/amazonwaf_test.py @@ -1,6 +1,6 @@ import unittest -from pydantic.error_wrappers import ValidationError +from pydantic import ValidationError from capmonstercloudclient.requests import AmazonWafRequest diff --git a/test/basilisk_test.py b/test/basilisk_test.py index 499b11f..0f0040a 100644 --- a/test/basilisk_test.py +++ b/test/basilisk_test.py @@ -1,6 +1,6 @@ import unittest -from pydantic.error_wrappers import ValidationError +from pydantic import ValidationError from capmonstercloudclient.requests import BasiliskCustomTaskRequest diff --git a/test/castle_test.py b/test/castle_test.py index b8a6063..ed30958 100644 --- a/test/castle_test.py +++ b/test/castle_test.py @@ -1,6 +1,6 @@ import unittest -from pydantic.error_wrappers import ValidationError +from pydantic import ValidationError from capmonstercloudclient.requests import CastleCustomTaskRequest diff --git a/test/cit_test.py b/test/cit_test.py index 6782662..3a298a3 100644 --- a/test/cit_test.py +++ b/test/cit_test.py @@ -1,7 +1,7 @@ import unittest import urllib import base64 -from pydantic.error_wrappers import ValidationError +from pydantic import ValidationError from capmonstercloudclient.requests import RecognitionComplexImageTaskRequest from capmonstercloudclient.exceptions import ( TaskNotDefinedError, diff --git a/test/cmclient_test.py b/test/cmclient_test.py index 5e5562c..7a4e8d1 100644 --- a/test/cmclient_test.py +++ b/test/cmclient_test.py @@ -10,6 +10,7 @@ api_key = os.getenv("API_KEY") +@unittest.skipUnless(api_key, "API_KEY not set; skipping live integration test") class InstanceRequestTest(unittest.IsolatedAsyncioTestCase): def testSuccessResponse(self): options = ClientOptions(api_key=api_key) diff --git a/test/datadome_test.py b/test/datadome_test.py index 37e502c..c8c2ba9 100644 --- a/test/datadome_test.py +++ b/test/datadome_test.py @@ -1,6 +1,6 @@ import unittest -from pydantic.error_wrappers import ValidationError +from pydantic import ValidationError from capmonstercloudclient.requests import DataDomeCustomTaskRequest, ProxyInfo diff --git a/test/fc_image_test.py b/test/fc_image_test.py index ed5ee2c..bc9d6b5 100644 --- a/test/fc_image_test.py +++ b/test/fc_image_test.py @@ -1,6 +1,6 @@ import unittest -from pydantic.error_wrappers import ValidationError +from pydantic import ValidationError from capmonstercloudclient.requests import FunCaptchaComplexImageTaskRequest from capmonstercloudclient.exceptions import ( NumbersImagesErrors, diff --git a/test/friendly_test.py b/test/friendly_test.py new file mode 100644 index 0000000..c6a9169 --- /dev/null +++ b/test/friendly_test.py @@ -0,0 +1,99 @@ +import unittest + +from pydantic import ValidationError +from capmonstercloudclient.requests import FriendlyCustomTaskRequest, ProxyInfo + + +class FriendlyCustomTaskRequestTest(unittest.TestCase): + websiteUrlExample = "https://example.com" + websiteKeyExample = "FCMSITEKEY000000000000000000000000000000" + userAgentExample = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36" + apiGetLibExample = "https://cdn.jsdelivr.net/npm/friendly-challenge@0.9.18/widget.module.min.js" + + def setUp(self): + self.proxy = ProxyInfo( + proxyType="http", + proxyAddress="8.8.8.8", + proxyPort=8080, + proxyLogin="proxyLoginHere", + proxyPassword="proxyPasswordHere" + ) + + def test_friendly_request_required_fields(self): + required_fields = ["type", "class", "websiteURL", "websiteKey", "metadata"] + metadata_required_fields = ["apiGetLib"] + metadata_example = { + "apiGetLib": self.apiGetLibExample, + } + request = FriendlyCustomTaskRequest( + websiteUrl=self.websiteUrlExample, + websiteKey=self.websiteKeyExample, + metadata=metadata_example, + ) + task_dictionary = request.getTaskDict() + for f in required_fields: + self.assertTrue( + f in list(task_dictionary.keys()), + msg=f'Required captcha input key "{f}" does not include to request.', + ) + for f in metadata_required_fields: + self.assertTrue( + f in list(task_dictionary["metadata"].keys()), + msg=f'Required captcha input key "{f}" does not include to request.', + ) + self.assertEqual(task_dictionary["class"], "friendly") + self.assertEqual(task_dictionary["type"], "CustomTask") + + def test_friendly_metadata_validation(self): + base_kwargs = { + "websiteUrl": self.websiteUrlExample, + "websiteKey": self.websiteKeyExample, + "metadata": {}, + } + self.assertRaises(TypeError, FriendlyCustomTaskRequest, **base_kwargs) + base_kwargs["metadata"]["apiGetLib"] = self.apiGetLibExample + FriendlyCustomTaskRequest(**base_kwargs) + + def test_friendly_missing_fields(self): + metadata_example = { + "apiGetLib": self.apiGetLibExample, + } + base_kwargs = {} + self.assertRaises(ValidationError, FriendlyCustomTaskRequest, **base_kwargs) + base_kwargs.update({"websiteUrl": self.websiteUrlExample}) + self.assertRaises(ValidationError, FriendlyCustomTaskRequest, **base_kwargs) + base_kwargs.update({"websiteKey": self.websiteKeyExample}) + self.assertRaises(ValidationError, FriendlyCustomTaskRequest, **base_kwargs) + base_kwargs.update({"metadata": metadata_example}) + FriendlyCustomTaskRequest(**base_kwargs) + + def test_friendly_optional_proxy_and_useragent(self): + metadata_example = { + "apiGetLib": self.apiGetLibExample, + } + request = FriendlyCustomTaskRequest( + websiteUrl=self.websiteUrlExample, + websiteKey=self.websiteKeyExample, + metadata=metadata_example, + proxy=self.proxy, + userAgent=self.userAgentExample, + ) + task_dictionary = request.getTaskDict() + self.assertEqual(task_dictionary["proxyType"], self.proxy.proxyType) + self.assertEqual(task_dictionary["userAgent"], self.userAgentExample) + + def test_friendly_proxy_not_required(self): + metadata_example = { + "apiGetLib": self.apiGetLibExample, + } + request = FriendlyCustomTaskRequest( + websiteUrl=self.websiteUrlExample, + websiteKey=self.websiteKeyExample, + metadata=metadata_example, + ) + task_dictionary = request.getTaskDict() + self.assertNotIn("proxyType", task_dictionary) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/hc_image_test.py b/test/hc_image_test.py index d224888..a775f40 100644 --- a/test/hc_image_test.py +++ b/test/hc_image_test.py @@ -1,6 +1,6 @@ import unittest -from pydantic.error_wrappers import ValidationError +from pydantic import ValidationError from capmonstercloudclient.requests import HcaptchaComplexImageTaskRequest from capmonstercloudclient.exceptions import ( NumbersImagesErrors, diff --git a/test/imperva_request_test.py b/test/imperva_request_test.py index 0edf1ea..236c999 100644 --- a/test/imperva_request_test.py +++ b/test/imperva_request_test.py @@ -11,7 +11,7 @@ class ImpervaRequestTest(unittest.TestCase): incapsulaScriptUrlExample = ( "dmFyIF8weGQ2ZmU9Wydce..eDUzXHg2YVx4NGYnKV09XzB4Mjk3MTIxO319KCkpOw==" ) - incapsulaCookieExample = "l/LsGnrvyB9lNhXI8borDKa2IGcAAAAAX0qAEHheCWuNDquzwb44cw=" + incapsulaCookiesExample = "l/LsGnrvyB9lNhXI8borDKa2IGcAAAAAX0qAEHheCWuNDquzwb44cw=" reese84UrlEndpointExample = "Built-with-the-For-hopence-Hurleysurfecting-the-" def setUp(self): @@ -27,10 +27,10 @@ def test_imperva( self, ): required_fields = ["type", "websiteURL", "metadata", "proxyType", "proxyAddress", "proxyPort", "proxyLogin", "proxyPassword"] - metadata_required_fields = ["incapsulaScriptUrl", "incapsulaCookie"] + metadata_required_fields = ["incapsulaScriptUrl", "incapsulaCookies"] metadata_example = { "incapsulaScriptUrl": self.incapsulaScriptUrlExample, - "incapsulaCookie": self.incapsulaCookieExample, + "incapsulaCookies": self.incapsulaCookiesExample, } request = ImpervaCustomTaskRequest( websiteUrl=self.websiteUrlExample, metadata=metadata_example, proxy=self.proxy @@ -54,7 +54,7 @@ def test_imperva_metadata( self.assertRaises(TypeError, ImpervaCustomTaskRequest, **base_kwargs) base_kwargs["metadata"]["incapsulaScriptUrl"] = self.incapsulaScriptUrlExample self.assertRaises(TypeError, ImpervaCustomTaskRequest, **base_kwargs) - base_kwargs["metadata"]["incapsulaCookie"] = self.incapsulaCookieExample + base_kwargs["metadata"]["incapsulaCookies"] = self.incapsulaCookiesExample ImpervaCustomTaskRequest(**base_kwargs) base_kwargs["metadata"]["reese84UrlEndpoint"] = self.reese84UrlEndpointExample ImpervaCustomTaskRequest(**base_kwargs) @@ -66,7 +66,7 @@ def test_imperva_missing( base_kwargs = {} metadata_example = { "incapsulaScriptUrl": self.incapsulaScriptUrlExample, - "incapsulaCookie": self.incapsulaCookieExample, + "incapsulaCookies": self.incapsulaCookiesExample, } self.assertRaises(RuntimeError, ImpervaCustomTaskRequest, **base_kwargs) base_kwargs.update({"proxy": self.proxy}) diff --git a/test/mtcaptcha_test.py b/test/mtcaptcha_test.py index ea9ad23..4f4d00a 100644 --- a/test/mtcaptcha_test.py +++ b/test/mtcaptcha_test.py @@ -1,6 +1,6 @@ import unittest -from pydantic.error_wrappers import ValidationError +from pydantic import ValidationError from capmonstercloudclient.requests import MTCaptchaRequest diff --git a/test/prosopo_test.py b/test/prosopo_test.py index 38e8290..4346374 100644 --- a/test/prosopo_test.py +++ b/test/prosopo_test.py @@ -1,6 +1,6 @@ import unittest -from pydantic.error_wrappers import ValidationError +from pydantic import ValidationError from capmonstercloudclient.requests import ProsopoTaskRequest diff --git a/test/recaptchaV3Enterprise_test.py b/test/recaptchaV3Enterprise_test.py index 2c7ce32..d774d43 100644 --- a/test/recaptchaV3Enterprise_test.py +++ b/test/recaptchaV3Enterprise_test.py @@ -1,6 +1,6 @@ import unittest -from pydantic.error_wrappers import ValidationError +from pydantic import ValidationError from capmonstercloudclient.requests import RecaptchaV3EnterpriseRequest diff --git a/test/recaptcha_image_test.py b/test/recaptcha_image_test.py index 05b772a..050ddc5 100644 --- a/test/recaptcha_image_test.py +++ b/test/recaptcha_image_test.py @@ -1,6 +1,6 @@ import unittest -from pydantic.error_wrappers import ValidationError +from pydantic import ValidationError from capmonstercloudclient.requests import RecaptchaComplexImageTaskRequest from capmonstercloudclient.exceptions import ( NumbersImagesErrors, diff --git a/test/temu_test.py b/test/temu_test.py index c3f415f..0922f8e 100644 --- a/test/temu_test.py +++ b/test/temu_test.py @@ -1,6 +1,6 @@ import unittest -from pydantic.error_wrappers import ValidationError +from pydantic import ValidationError from capmonstercloudclient.requests import TemuCustomTaskRequest diff --git a/test/tendi_test.py b/test/tendi_test.py index 77f145e..d9d653d 100644 --- a/test/tendi_test.py +++ b/test/tendi_test.py @@ -1,6 +1,6 @@ import unittest -from pydantic.error_wrappers import ValidationError +from pydantic import ValidationError from capmonstercloudclient.requests import TenDiCustomTaskRequest diff --git a/test/yidun_test.py b/test/yidun_test.py index fff8316..fd716a0 100644 --- a/test/yidun_test.py +++ b/test/yidun_test.py @@ -1,6 +1,6 @@ import unittest -from pydantic.error_wrappers import ValidationError +from pydantic import ValidationError from capmonstercloudclient.requests import YidunRequest From d3a9408c2b002dcf992a977f1df1a0786a2c250c Mon Sep 17 00:00:00 2001 From: "pavel.grinkevich" Date: Thu, 30 Jul 2026 20:04:15 +0000 Subject: [PATCH 2/3] update readme --- README.md | 134 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 91 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index d8488b1..0ad466c 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,106 @@ # Zennolab.CapMonsterCloud.Client -> [!IMPORTANT] -> This repository has been moved and is no longer maintained. -> -> New repository: [https://github.com/CapMonsterCloud/client-python](https://github.com/CapMonsterCloud/client-python) -> -> Please update your references and use the new repository for all future development. +Official Python client library for [capmonster.cloud](https://capmonster.cloud/) — an AI-powered CAPTCHA solving and anti-bot bypass API. +## Installation -Official python client library for [capmonster.cloud](https://capmonster.cloud/) captcha recognition service +```bash +python3 -m pip install capmonstercloudclient +``` -## Installation +## 🚀 Quick Start - python3 -m pip install capmonstercloudclient +1. Get your API key in the [CapMonster Cloud Dashboard](https://dash.capmonster.cloud/). +2. Install the package (see above). +3. Copy a snippet below, replace `YOUR_API_KEY`, and run it. -## Usage +### Bypass reCAPTCHA v2 -*** - import asyncio +```python +import asyncio - from capmonstercloudclient import CapMonsterClient, ClientOptions - from capmonstercloudclient.requests import RecaptchaV2ProxylessRequest +from capmonstercloudclient import CapMonsterClient, ClientOptions +from capmonstercloudclient.requests import RecaptchaV2Request - client_options = ClientOptions(api_key=) + +async def main(): + client_options = ClientOptions(api_key="YOUR_API_KEY") cap_monster_client = CapMonsterClient(options=client_options) - async def solve_captcha(): - return await cap_monster_client.solve_captcha(recaptcha2request) - - recaptcha2request = RecaptchaV2ProxylessRequest(websiteUrl="https://lessons.zennolab.com/captchas/recaptcha/v2_simple.php?level=high", - websiteKey="6Lcg7CMUAAAAANphynKgn9YAgA4tQ2KI_iqRyTwd") - - responses = asyncio.run(solve_captcha()) - print(responses) -*** - -Supported captcha recognition requests: - -- [GeeTestProxylessRequest](https://zenno.link/doc-geetest-en) -- [GeeTestRequest](https://zenno.link/doc-geetest-proxy-en) -- [ImageToTextRequest](https://zenno.link/doc-ImageToTextTask-en) -- [RecaptchaV2ProxylessRequest](https://zenno.link/doc-recaptcha2-en) -- [RecaptchaV2Request](https://zenno.link/doc-recaptcha2-proxy-en) -- [RecaptchaV3ProxylessRequest](https://zenno.link/doc-recaptcha3-en) -- [RecaptchaV2EnterpriseProxylessRequest](https://zenno.link/doc-recaptcha2e-en) -- [RecaptchaV2EnterpriseRequest](https://zenno.link/doc-recaptcha2e-proxy-en) -- [TurnstileProxylessRequest](https://zenno.link/doc-turnstile-en) -- [TurnstileRequest](https://zenno.link/doc-turnstile-proxy-en) -- [RecaptchaComplexImageTaskRequest](https://zenno.link/doc-complextask-rc-en) -- [DataDomeCustomTaskRequest](https://docs.capmonster.cloud/docs/captchas/datadome) -- [TenDiCustomTaskRequest](https://docs.capmonster.cloud/docs/captchas/tendi) -- [BasiliskCustomTaskRequest](https://docs.capmonster.cloud/docs/captchas/Basilisk-task) + recaptcha2_request = RecaptchaV2Request( + websiteUrl="https://lessons.zennolab.com/captchas/recaptcha/v2_simple.php?level=high", + websiteKey="6Lcg7CMUAAAAANphynKgn9YAgA4tQ2KI_iqRyTwd", + ) + + solution = await cap_monster_client.solve_captcha(recaptcha2_request) + print(solution) # {'gRecaptchaResponse': '...'} + + +asyncio.run(main()) +``` + +### Bypass Cloudflare Turnstile + +```python +import asyncio + +from capmonstercloudclient import CapMonsterClient, ClientOptions +from capmonstercloudclient.requests import TurnstileRequest + + +async def main(): + client_options = ClientOptions(api_key="YOUR_API_KEY") + cap_monster_client = CapMonsterClient(options=client_options) + + turnstile_request = TurnstileRequest( + websiteURL="https://tsinvisble.zlsupport.com", + websiteKey="0x4AAAAAAABUY0VLtOUMAHxE", + ) + + solution = await cap_monster_client.solve_captcha(turnstile_request) + print(solution) # {'token': '...'} + + +asyncio.run(main()) +``` + +More end-to-end examples (proxies, image-based captchas, custom anti-bot tasks) are available in the [`examples/`](examples/) directory. + +## ⚡ Supported CAPTCHA Recognition Requests + +### Classic captcha tasks + +- [ImageToTextRequest](https://docs.capmonster.cloud/docs/captchas/image-to-text) +- [RecaptchaV2Request](https://docs.capmonster.cloud/docs/captchas/no-captcha-task) +- [RecaptchaV2EnterpriseRequest](https://docs.capmonster.cloud/docs/captchas/recaptcha-v2-enterprise-task) +- [RecaptchaV3ProxylessRequest](https://docs.capmonster.cloud/docs/captchas/recaptcha-v3-task) +- [FuncaptchaRequest](https://docs.capmonster.cloud/docs/captchas/funcaptcha-task) +- [GeetestRequest](https://docs.capmonster.cloud/docs/captchas/geetest-task) +- [TurnstileRequest — Cloudflare Turnstile](https://docs.capmonster.cloud/docs/captchas/turnstile-task) +- [TurnstileRequest — Cloudflare Challenge](https://docs.capmonster.cloud/docs/captchas/turnstile-challenge-task) +- [TurnstileRequest — Cloudflare Waiting Room](https://docs.capmonster.cloud/docs/captchas/turnstile-waitroom-task) - [AmazonWafRequest](https://docs.capmonster.cloud/docs/captchas/amazon-task) - [BinanceTaskRequest](https://docs.capmonster.cloud/docs/captchas/binance) -- [BinanceTaskProxylessRequest](https://docs.capmonster.cloud/docs/captchas/binance) +- [MTCaptchaRequest](https://docs.capmonster.cloud/docs/captchas/mtcaptcha-task) +- [ProsopoTaskRequest](https://docs.capmonster.cloud/docs/captchas/prosopo-task) +- [YidunRequest](https://docs.capmonster.cloud/docs/captchas/yidun-task) + +### Custom tasks (anti-bot / WAF / custom challenge systems) + +- [AlibabaCustomTaskRequest](https://docs.capmonster.cloud/docs/captchas/alibaba-task) +- [AltchaCustomTaskRequest](https://docs.capmonster.cloud/docs/captchas/altcha-task) +- [BasiliskCustomTaskRequest](https://docs.capmonster.cloud/docs/captchas/Basilisk-task) +- [DataDomeCustomTaskRequest](https://docs.capmonster.cloud/docs/captchas/datadome) +- [FriendlyCustomTaskRequest](https://docs.capmonster.cloud/docs/captchas/friendly-task) +- [HuntCustomTaskRequest](https://docs.capmonster.cloud/docs/captchas/hunt-task) - [ImpervaCustomTaskRequest](https://docs.capmonster.cloud/docs/captchas/incapsula) -- [ImpervaCustomTaskProxylessRequest](https://docs.capmonster.cloud/docs/captchas/incapsula) +- [TenDiCustomTaskRequest](https://docs.capmonster.cloud/docs/captchas/tendi) +- [TspdCustomTaskRequest](https://docs.capmonster.cloud/docs/captchas/tspd-task) + +### Complex image tasks (grid / dynamic image selection tasks) + +- [RecaptchaComplexImageTaskRequest](https://docs.capmonster.cloud/docs/captchas/recaptcha-click) +- [RecognitionComplexImageTaskRequest](https://docs.capmonster.cloud/docs/captchas/compleximage/rotation/baidu) + +--- +**[Official Documentation](https://docs.capmonster.cloud/docs/getting-start/)** | **[Register Account](https://dash.capmonster.cloud/)** From 63042826de1db35d74cef28d309c8ba34df45c82 Mon Sep 17 00:00:00 2001 From: "pavel.grinkevich" Date: Fri, 31 Jul 2026 16:54:08 +0000 Subject: [PATCH 3/3] docstrings and bugfixes --- .gitignore | 3 +- .../requests/AlibabaCustomTaskRequest.py | 15 ++- .../requests/AltchaCustomTaskRequest.py | 20 +++- .../requests/AmazonWafRequest.py | 95 +++++++++++++++---- .../requests/BasiliskCustomTaskRequest.py | 15 ++- .../requests/BinanceTaskRequest.py | 22 ++++- .../requests/CastleCustomTaskRequest.py | 18 +++- .../requests/ComplexImageTaskBase.py | 26 +++-- .../requests/CustomTaskRequestBase.py | 27 +++++- .../requests/DataDomeCustomTaskRequest.py | 27 ++++-- .../requests/FriendlyCustomTaskRequest.py | 19 +++- .../requests/FuncaptchaComplexImageTask.py | 22 ++++- .../requests/FuncaptchaRequest.py | 36 +++++-- .../requests/GeetestRequest.py | 38 ++++++-- .../requests/HcaptchaComplexImageTask.py | 30 +++++- .../requests/HcaptchaRequest.py | 34 +++++-- .../requests/HuntCustomTaskRequest.py | 17 +++- .../requests/ImageToTextRequest.py | 36 +++++-- .../requests/ImpervaCustomTaskRequest.py | 16 +++- .../requests/MTCaptchaRequest.py | 27 ++++-- .../requests/ProsopoTaskRequest.py | 15 ++- .../requests/RecaptchaComplexImageTask.py | 30 ++++-- .../requests/RecaptchaV2EnterpiseRequest.py | 44 +++++++-- .../requests/RecaptchaV2Request.py | 35 +++++-- .../requests/RecaptchaV3EnterpriseRequest.py | 24 ++++- .../requests/RecaptchaV3ProxylessRequest.py | 29 +++++- .../RecognitionComplexImageTaskRequest.py | 19 +++- .../requests/TemuCustomTaskRequest.py | 16 +++- .../requests/TenDiCustomTaskRequest.py | 17 +++- .../requests/TspdCustomTaskRequest.py | 21 +++- .../requests/TurnstileRequest.py | 47 ++++++--- .../requests/YidunRequest.py | 41 ++++++-- .../requests/baseRequest.py | 16 +++- .../requests/baseRequestWithProxy.py | 11 ++- capmonstercloud_client/requests/enums.py | 13 +++ capmonstercloud_client/requests/proxy_info.py | 47 ++++++--- test/datadome_test.py | 22 +---- test/requests_generation_test.py | 20 +++- 38 files changed, 801 insertions(+), 209 deletions(-) diff --git a/.gitignore b/.gitignore index c2ab60a..9c17b43 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ build/ env/ venv/ capmonstercloudclient.egg-info/ -.mcp.json \ No newline at end of file +.mcp.json +.claude/ \ No newline at end of file diff --git a/capmonstercloud_client/requests/AlibabaCustomTaskRequest.py b/capmonstercloud_client/requests/AlibabaCustomTaskRequest.py index 18666cf..b8772d3 100644 --- a/capmonstercloud_client/requests/AlibabaCustomTaskRequest.py +++ b/capmonstercloud_client/requests/AlibabaCustomTaskRequest.py @@ -9,8 +9,19 @@ } class AlibabaCustomTaskRequest(CustomTaskRequestBase): - captchaClass: str = Field(default='alibaba') - metadata: Dict[str, Union[str, bool]] + """ + Represents a payload structure for solving Alibaba custom captcha challenges. + + Attributes: + captchaClass: The constant string value identifying the captcha + class as "alibaba". + metadata: A dictionary of Alibaba-specific parameters. Requires sceneId + and prefix; userId, userUserId, verifyType, region, UserCertifyId, + apiGetLib, and cookieRequired are optional, needed only for sites + that use them. + """ + captchaClass: str = Field(default='alibaba', description='The constant string value identifying the captcha class as "alibaba".') + metadata: Dict[str, Union[str, bool]] = Field(..., description='A dictionary of Alibaba-specific parameters. Requires sceneId and prefix; userId, userUserId, verifyType, region, UserCertifyId, apiGetLib, and cookieRequired are optional, needed only for sites that use them.') @field_validator('metadata') @classmethod diff --git a/capmonstercloud_client/requests/AltchaCustomTaskRequest.py b/capmonstercloud_client/requests/AltchaCustomTaskRequest.py index d84d3be..71e2664 100644 --- a/capmonstercloud_client/requests/AltchaCustomTaskRequest.py +++ b/capmonstercloud_client/requests/AltchaCustomTaskRequest.py @@ -4,9 +4,23 @@ from .CustomTaskRequestBase import CustomTaskRequestBase class AltchaCustomTaskRequest(CustomTaskRequestBase): - captchaClass: str = Field(default='altcha') - websiteKey: str = Field() - metadata : Dict[str, str] + """ + Represents a payload structure for solving Altcha proof-of-work + captcha challenges via a custom module. + + Attributes: + captchaClass: The class (subtype) identifier of the custom + module, constant "altcha" for this task. + websiteKey: The site key associated with the Altcha challenge + on the webpage. An empty string is allowed for this task. + metadata: A dictionary containing the Altcha challenge parameters + (challenge, iterations, salt, signature) extracted from the + webpage. + """ + + captchaClass: str = Field(default='altcha', description='Class (subtype) identifier of the custom module, constant "altcha".') + websiteKey: str = Field(description='Site key associated with the Altcha challenge on the webpage. An empty string is allowed for this task.') + metadata: Dict[str, str] = Field(description='Altcha challenge parameters: challenge, iterations, salt, and signature.') @field_validator('metadata') @classmethod diff --git a/capmonstercloud_client/requests/AmazonWafRequest.py b/capmonstercloud_client/requests/AmazonWafRequest.py index 6104f1b..62b90b9 100644 --- a/capmonstercloud_client/requests/AmazonWafRequest.py +++ b/capmonstercloud_client/requests/AmazonWafRequest.py @@ -1,35 +1,98 @@ from typing import Dict, Union, Optional -from pydantic import Field +from pydantic import Field, model_validator from .baseRequestWithProxy import BaseRequestWithProxy class AmazonWafRequest(BaseRequestWithProxy): - type: str = 'AmazonTask' - websiteUrl: str - challengeScript: str - captchaScript: str - websiteKey: str - context: str - iv: str - cookieSolution: Optional[bool] = Field(default=None) + """ + Represents a payload structure for solving Amazon AWS WAF captcha challenges. + + The live API accepts three mutually exclusive parameter sets: + Option 1 (visible captcha): websiteUrl, websiteKey, captchaScript + required; challengeScript/context/iv must not be set. + Option 2 (challenge + captcha): websiteUrl, challengeScript, websiteKey, + context, iv required; captchaScript optional. + Option 3 (invisible/challenge-only): websiteUrl, challengeScript + required; context and iv must be empty strings; websiteKey and + captchaScript must not be set. + + Attributes: + type: The constant string value identifying the task type as "AmazonTask". + websiteUrl: The URL of the webpage where the Amazon AWS WAF challenge + is presented. + challengeScript: Link to challenge.js served by AWS WAF on the target + page. Required for Options 2 and 3; must not be set for Option 1. + captchaScript: Link to captcha.js / jsapi.js served by AWS WAF on the + target page. Required for Option 1, optional for Option 2, must + not be set for Option 3. + websiteKey: The site key associated with the AWS WAF challenge. + Required for Options 1 and 2; must not be set for Option 3. + context: The context token provided by AWS WAF, used to correlate + the challenge with a specific session. Required for Option 2; + must be an empty string for Option 3; must not be set for Option 1. + iv: The initialization vector value provided by AWS WAF as part + of the challenge payload. Required for Option 2; must be an + empty string for Option 3; must not be set for Option 1. + cookieSolution: When set to True, requests the solution to be + returned as a ready-to-use cookie instead of a token. + userAgent: Browser User-Agent to emulate. Only used for Option 1. + Pass only a current Windows OS UA. + """ + type: str = Field(default='AmazonTask', description='The constant string value identifying the task type as "AmazonTask".') + websiteUrl: str = Field(..., description='The URL of the webpage where the Amazon AWS WAF challenge is presented.') + challengeScript: Optional[str] = Field(default=None, description='Link to challenge.js served by AWS WAF on the target page. Required for Options 2 and 3; must not be set for Option 1.') + captchaScript: Optional[str] = Field(default=None, description='Link to captcha.js / jsapi.js served by AWS WAF on the target page. Required for Option 1, optional for Option 2, must not be set for Option 3.') + websiteKey: Optional[str] = Field(default=None, description='The site key associated with the AWS WAF challenge. Required for Options 1 and 2; must not be set for Option 3.') + context: Optional[str] = Field(default=None, description='The context token provided by AWS WAF, used to correlate the challenge with a specific session. Required for Option 2; must be an empty string for Option 3; must not be set for Option 1.') + iv: Optional[str] = Field(default=None, description='The initialization vector value provided by AWS WAF as part of the challenge payload. Required for Option 2; must be an empty string for Option 3; must not be set for Option 1.') + cookieSolution: Optional[bool] = Field(default=None, description='When set to True, requests the solution to be returned as a ready-to-use cookie instead of a token.') + userAgent: Optional[str] = Field(default=None, description='Browser User-Agent to emulate. Only used for Option 1. Pass only a current Windows OS UA.') + + @model_validator(mode='after') + def validate_amazon_waf_variant(self): + if self.challengeScript is None: + # Option 1: visible captcha, no challenge.js involved. + if self.websiteKey is None or self.captchaScript is None: + raise ValueError('Expect that "websiteKey" and "captchaScript" will be filled ' + 'when "challengeScript" is not provided (Option 1).') + if self.context is not None or self.iv is not None: + raise ValueError('"context" and "iv" are not used when "challengeScript" is not provided (Option 1).') + elif self.websiteKey is None and self.captchaScript is None: + # Option 3: invisible/challenge-only captcha. + if self.context != '' or self.iv != '': + raise ValueError('Expect that "context" and "iv" will be empty strings ' + 'when only "challengeScript" is provided (Option 3).') + else: + # Option 2: challenge + captcha. + if self.websiteKey is None or self.context is None or self.iv is None: + raise ValueError('Expect that "websiteKey", "context" and "iv" will be filled ' + 'when "challengeScript" is provided together with "websiteKey" (Option 2).') + return self def getTaskDict(self) -> Dict[str, Union[str, int, bool]]: task = {} task['type'] = self.type task['websiteURL'] = self.websiteUrl - task['challengeScript'] = self.challengeScript - task['captchaScript'] = self.captchaScript - task['websiteKey'] = self.websiteKey - task['context'] = self.context - task['iv'] = self.iv - + if self.challengeScript is not None: + task['challengeScript'] = self.challengeScript + if self.captchaScript is not None: + task['captchaScript'] = self.captchaScript + if self.websiteKey is not None: + task['websiteKey'] = self.websiteKey + if self.context is not None: + task['context'] = self.context + if self.iv is not None: + task['iv'] = self.iv + if self.proxy: task['proxyType'] = self.proxy.proxyType task['proxyAddress'] = self.proxy.proxyAddress task['proxyPort'] = self.proxy.proxyPort task['proxyLogin'] = self.proxy.proxyLogin task['proxyPassword'] = self.proxy.proxyPassword - + if self.cookieSolution is not None: task['cookieSolution'] = self.cookieSolution + if self.userAgent is not None: + task['userAgent'] = self.userAgent return task \ No newline at end of file diff --git a/capmonstercloud_client/requests/BasiliskCustomTaskRequest.py b/capmonstercloud_client/requests/BasiliskCustomTaskRequest.py index 1d3602a..9f14f93 100644 --- a/capmonstercloud_client/requests/BasiliskCustomTaskRequest.py +++ b/capmonstercloud_client/requests/BasiliskCustomTaskRequest.py @@ -4,8 +4,19 @@ from .CustomTaskRequestBase import CustomTaskRequestBase class BasiliskCustomTaskRequest(CustomTaskRequestBase): - captchaClass: str = Field(default='Basilisk') - websiteKey: str = Field() + """ + Represents a payload structure for solving Basilisk custom captcha + challenges. + + Attributes: + captchaClass: The class (subtype) identifier of the custom module, + fixed to "Basilisk" for this task type. + websiteKey: The site key associated with the Basilisk captcha on + the webpage. + """ + + captchaClass: str = Field(default='Basilisk', description='Class (subtype) identifier of the custom module, fixed to "Basilisk".') + websiteKey: str = Field(..., description='Site key associated with the Basilisk captcha on the webpage.') def getTaskDict(self) -> Dict[str, Union[str, int, bool]]: task = {} diff --git a/capmonstercloud_client/requests/BinanceTaskRequest.py b/capmonstercloud_client/requests/BinanceTaskRequest.py index 0c99f9f..ec35896 100644 --- a/capmonstercloud_client/requests/BinanceTaskRequest.py +++ b/capmonstercloud_client/requests/BinanceTaskRequest.py @@ -4,11 +4,23 @@ from .baseRequestWithProxy import BaseRequestWithProxy class BinanceTaskRequest(BaseRequestWithProxy): - type: str = Field(default='BinanceTask') - websiteKey: str = Field() - websiteUrl: str = Field() - validateId: str = Field() - userAgent: Optional[str] = None + """ + Represents a payload structure for solving Binance's custom captcha challenge. + + Attributes: + type: The constant string value identifying the task type as "BinanceTask". + websiteKey: The site key associated with the Binance captcha challenge. + websiteUrl: The URL of the webpage containing the Binance captcha challenge. + validateId: A unique identifier for the specific captcha validation + attempt, issued by Binance for each challenge instance. + userAgent: Browser User-Agent to emulate. Pass only a current + Windows OS UA. + """ + type: str = Field(default='BinanceTask', description='The constant string value identifying the task type as "BinanceTask".') + websiteKey: str = Field(description="The site key associated with the Binance captcha challenge.") + websiteUrl: str = Field(description="The URL of the webpage containing the Binance captcha challenge.") + validateId: str = Field(description="A unique identifier for the specific captcha validation attempt, issued by Binance for each challenge instance.") + userAgent: Optional[str] = Field(default=None, description="Browser User-Agent to emulate. Pass only a current Windows OS UA.") def getTaskDict(self) -> Dict[str, Union[str, int, bool]]: task = {} diff --git a/capmonstercloud_client/requests/CastleCustomTaskRequest.py b/capmonstercloud_client/requests/CastleCustomTaskRequest.py index 79b555b..99e4830 100644 --- a/capmonstercloud_client/requests/CastleCustomTaskRequest.py +++ b/capmonstercloud_client/requests/CastleCustomTaskRequest.py @@ -4,9 +4,21 @@ from .CustomTaskRequestBase import CustomTaskRequestBase class CastleCustomTaskRequest(CustomTaskRequestBase): - captchaClass: str = Field(default='Castle') - websiteKey: str = Field() - metadata: Dict[str, Union[str, int]] + """ + Represents a payload structure for solving Castle challenges via a custom task. + + Attributes: + captchaClass: The constant string value identifying the captcha + class as "Castle". + websiteKey: The site key associated with the Castle challenge on the + webpage. + metadata: A dictionary of additional parameters required to solve the + challenge, including the worker script URL (wUrl), service worker + URL (swUrl), and an optional request count (count). + """ + captchaClass: str = Field(default='Castle', description='The constant string value identifying the captcha class as "Castle".') + websiteKey: str = Field(description='The site key associated with the Castle challenge on the webpage.') + metadata: Dict[str, Union[str, int]] = Field(description='A dictionary of additional parameters required to solve the challenge, including the worker script URL (wUrl), service worker URL (swUrl), and an optional request count (count).') @field_validator('metadata') @classmethod diff --git a/capmonstercloud_client/requests/ComplexImageTaskBase.py b/capmonstercloud_client/requests/ComplexImageTaskBase.py index b5ff097..2a17620 100644 --- a/capmonstercloud_client/requests/ComplexImageTaskBase.py +++ b/capmonstercloud_client/requests/ComplexImageTaskBase.py @@ -1,11 +1,25 @@ from typing import Optional, List +from pydantic import Field + from .baseRequest import BaseRequest class ComplexImageTaskRequestBase(BaseRequest): - captchaClass: str # Class(subtype) of ComplexImageTask - taskType: str = "ComplexImageTask" # Recognition task type - websiteUrl: Optional[str] = None # Address of a webpage with captcha - imagesUrls: Optional[List[str]] = None # Collection with image urls. Must be populated if not. - imagesBase64: Optional[List[str]] = None # Collection with base64 encoded images. Must be populated if not. - userAgent: Optional[str] = None # It is required that you use a signature of a modern browser \ No newline at end of file + """ + Represents a payload structure for solving complex image-based recognition tasks. + + Attributes: + captchaClass: The class (subtype) of the ComplexImageTask, identifying + the specific image recognition scenario to be solved. + taskType: The constant string value identifying the task type as + "ComplexImageTask". + websiteUrl: The URL of the webpage containing the captcha, if applicable. + imagesBase64: A collection of base64-encoded images to be recognized. + userAgent: Browser User-Agent to emulate. Pass only a current + Windows OS UA. + """ + captchaClass: str = Field(..., description='Class(subtype) of ComplexImageTask.') # Class(subtype) of ComplexImageTask + taskType: str = Field(default="ComplexImageTask", description='Recognition task type.') # Recognition task type + websiteUrl: Optional[str] = Field(default=None, description='Address of a webpage with captcha.') # Address of a webpage with captcha + imagesBase64: Optional[List[str]] = Field(default=None, description='Collection with base64 encoded images.') # Collection with base64 encoded images. + userAgent: Optional[str] = Field(default=None, description='Browser User-Agent to emulate. Pass only a current Windows OS UA.') \ No newline at end of file diff --git a/capmonstercloud_client/requests/CustomTaskRequestBase.py b/capmonstercloud_client/requests/CustomTaskRequestBase.py index a9a16f2..9d7524a 100644 --- a/capmonstercloud_client/requests/CustomTaskRequestBase.py +++ b/capmonstercloud_client/requests/CustomTaskRequestBase.py @@ -1,10 +1,27 @@ from typing import Optional, List +from pydantic import Field + from .baseRequestWithProxy import BaseRequestWithProxy class CustomTaskRequestBase(BaseRequestWithProxy): - captchaClass: str # Class(subtype) of ComplexImageTask - type: str = "CustomTask" # Recognition task type - websiteUrl: str # Address of a webpage with captcha - userAgent: Optional[str] = None - domains: Optional[List[str]] = None + """ + Base payload structure for CustomTask-family anti-bot/WAF challenges + (e.g. DataDome, Imperva, Basilisk, TenDI, Altcha, TSPD, HUNT, Alibaba, + Friendly Captcha), each selected via a fixed "class" discriminator value. + + Attributes: + captchaClass: The built-in class (subtype) discriminator identifying + which CustomTask variant to solve (e.g. "DataDome", "Imperva", "HUNT"). + type: The constant string value identifying the task type as "CustomTask". + websiteUrl: The URL of the webpage containing the captcha. + userAgent: Browser User-Agent to emulate. Pass only a current + Windows OS UA. + domains: A list of domains the returned cookies/solution should apply to. + """ + + captchaClass: str = Field(..., description='The built-in class (subtype) discriminator identifying which CustomTask variant to solve (e.g. "DataDome", "Imperva", "HUNT").') + type: str = Field(default="CustomTask", description='The constant string value identifying the task type as "CustomTask".') + websiteUrl: str = Field(..., description='Address of a webpage with captcha.') + userAgent: Optional[str] = Field(default=None, description='Browser User-Agent to emulate. Pass only a current Windows OS UA.') + domains: Optional[List[str]] = Field(default=None, description='A list of domains the returned cookies/solution should apply to.') diff --git a/capmonstercloud_client/requests/DataDomeCustomTaskRequest.py b/capmonstercloud_client/requests/DataDomeCustomTaskRequest.py index d097786..bbde287 100644 --- a/capmonstercloud_client/requests/DataDomeCustomTaskRequest.py +++ b/capmonstercloud_client/requests/DataDomeCustomTaskRequest.py @@ -3,8 +3,20 @@ from .CustomTaskRequestBase import CustomTaskRequestBase class DataDomeCustomTaskRequest(CustomTaskRequestBase): - captchaClass: str = Field(default='DataDome') - metadata : Dict[str, str] + """ + Represents a payload structure for solving DataDome custom challenges. + + Attributes: + captchaClass: The constant string value identifying the captcha + class as "DataDome". + metadata: A dictionary carrying DataDome-specific challenge data: + the required captchaUrl and datadomeCookie, plus the optional + datadomeVersion. + proxy: Proxy settings to route the request through. Required for + this task type — DataDome will not solve without your own proxy. + """ + captchaClass: str = Field(default='DataDome', description='The constant string value identifying the captcha class as "DataDome".') + metadata : Dict[str, str] = Field(..., description='A dictionary carrying DataDome-specific challenge data: the required captchaUrl and datadomeCookie, plus the optional datadomeVersion.') @field_validator('metadata') @classmethod @@ -13,14 +25,9 @@ def validate_metadata(cls, value): raise TypeError(f'Expect that datadomeCookie will be defined.') if value.get('datadomeVersion') is not None and not isinstance(value.get('datadomeVersion'), str): raise TypeError(f'Expected datadomeVersion to be str') - if value.get('captchaUrl') and value.get('htmlPageBase64'): - raise TypeError(f'Expected only one of [captchaUrl, htmlPageBase64]') - elif value.get('captchaUrl'): - return {i: value[i] for i in value if i != 'htmlPageBase64'} - elif value.get('htmlPageBase64'): - return {i: value[i] for i in value if i != 'captchaUrl'} - else: - raise TypeError(f'Expected one of [captchaUrl, htmlPageBase64]') + if value.get('captchaUrl') is None: + raise TypeError(f'Expect that captchaUrl will be defined.') + return value @model_validator(mode='before') def validate_datadome_proxy(cls, values): diff --git a/capmonstercloud_client/requests/FriendlyCustomTaskRequest.py b/capmonstercloud_client/requests/FriendlyCustomTaskRequest.py index b8e0756..f756368 100644 --- a/capmonstercloud_client/requests/FriendlyCustomTaskRequest.py +++ b/capmonstercloud_client/requests/FriendlyCustomTaskRequest.py @@ -4,9 +4,22 @@ from .CustomTaskRequestBase import CustomTaskRequestBase class FriendlyCustomTaskRequest(CustomTaskRequestBase): - captchaClass: str = Field(default='friendly') - websiteKey: str = Field() - metadata: Dict[str, str] + """ + Represents a payload structure for solving Friendly Captcha challenges. + + Attributes: + captchaClass: The constant string value "friendly" identifying + this custom task as a Friendly Captcha challenge. + websiteKey: The site key associated with the Friendly Captcha + widget on the webpage. + metadata: Must contain the "apiGetLib" entry — the URL of the Friendly + Captcha widget script loaded on the page (widget.module.min.js for + V1, site.min.js for V2), used to detect the captcha version. + """ + + captchaClass: str = Field(default='friendly', description='Constant string "friendly" identifying this custom task as a Friendly Captcha challenge.') + websiteKey: str = Field(..., description='Site key associated with the Friendly Captcha widget on the webpage.') + metadata: Dict[str, str] = Field(..., description='Additional task parameters, must include the "apiGetLib" string entry.') @field_validator('metadata') @classmethod diff --git a/capmonstercloud_client/requests/FuncaptchaComplexImageTask.py b/capmonstercloud_client/requests/FuncaptchaComplexImageTask.py index dcb1341..0c4637e 100644 --- a/capmonstercloud_client/requests/FuncaptchaComplexImageTask.py +++ b/capmonstercloud_client/requests/FuncaptchaComplexImageTask.py @@ -1,13 +1,27 @@ -from typing import Dict, Union +from typing import Dict, List, Optional, Union from pydantic import Field, field_validator from .ComplexImageTaskBase import ComplexImageTaskRequestBase from ..exceptions import NumbersImagesErrors, ZeroImagesErrors, TaskNotDefinedError class FunCaptchaComplexImageTaskRequest(ComplexImageTaskRequestBase): - - captchaClass: str = Field(default='funcaptcha') - metadata : Dict[str, str] + """ + Represents a payload structure for solving FunCaptcha (Arkose Labs) + complex image challenges. + + Attributes: + captchaClass: The constant string value identifying the complex + image task subtype as "funcaptcha". + metadata: A dictionary describing the FunCaptcha challenge, which + must include a "Task" key naming the specific image challenge + to solve (e.g. matching, rotating, or selecting images). + imagesUrls: A collection of image URLs to be recognized. Must be + populated if imagesBase64 is not. + """ + + captchaClass: str = Field(default='funcaptcha', description='The constant string value identifying the complex image task subtype as "funcaptcha".') + metadata : Dict[str, str] = Field(..., description='A dictionary describing the FunCaptcha challenge, which must include a "Task" key naming the specific image challenge to solve.') + imagesUrls: Optional[List[str]] = Field(default=None, description='Collection with image urls. Must be populated if imagesBase64 is not.') @field_validator('metadata') @classmethod diff --git a/capmonstercloud_client/requests/FuncaptchaRequest.py b/capmonstercloud_client/requests/FuncaptchaRequest.py index 9646c8e..ba66971 100644 --- a/capmonstercloud_client/requests/FuncaptchaRequest.py +++ b/capmonstercloud_client/requests/FuncaptchaRequest.py @@ -4,13 +4,33 @@ from .baseRequestWithProxy import BaseRequestWithProxy class FuncaptchaRequest(BaseRequestWithProxy): - type: str = Field(default='FunCaptchaTask') - websiteUrl: str - websitePublicKey: str - funcaptchaApiJSSubdomain: Optional[str] = Field(default=None) - data: Optional[str] = Field(default=None) - cookies: Optional[str] = Field(default=None) - + """ + Represents a payload structure for solving FunCaptcha (Arkose Labs) challenges. + + Attributes: + type: The constant string value identifying the task type as "FunCaptchaTask". + websiteUrl: The URL of the webpage containing the FunCaptcha challenge. + websitePublicKey: The public key (site key) associated with the + FunCaptcha challenge on the webpage. + funcaptchaApiJSSubdomain: A custom subdomain used by some FunCaptcha + implementations to load the API JS script. Required only if the + target website uses a non-default subdomain. + data: Additional custom data required by certain FunCaptcha + implementations, typically passed as a JSON string with extra + parameters such as "blob". + cookies: Cookies to be used when accessing the target webpage, + provided as a string of key-value pairs. + userAgent: Browser User-Agent to emulate. Pass only a current + Windows OS UA. + """ + type: str = Field(default='FunCaptchaTask', description='The task type identifier, "FunCaptchaTask".') + websiteUrl: str = Field(..., description='The URL of the webpage containing the FunCaptcha challenge.') + websitePublicKey: str = Field(..., description='The public key (site key) associated with the FunCaptcha challenge on the webpage.') + funcaptchaApiJSSubdomain: Optional[str] = Field(default=None, description='A custom subdomain used by some FunCaptcha implementations to load the API JS script.') + data: Optional[str] = Field(default=None, description='Additional custom data required by certain FunCaptcha implementations (e.g. a JSON string containing "blob").') + cookies: Optional[str] = Field(default=None, description='Cookies to be used when accessing the target webpage, provided as a string of key-value pairs.') + userAgent: Optional[str] = Field(default=None, description='Browser User-Agent to emulate. Pass only a current Windows OS UA.') + def getTaskDict(self) -> Dict[str, Union[str, int, bool]]: task = {} task['type'] = self.type @@ -29,4 +49,6 @@ def getTaskDict(self) -> Dict[str, Union[str, int, bool]]: task['data'] = self.data if self.cookies is not None: task['cookies'] = self.cookies + if self.userAgent is not None: + task['userAgent'] = self.userAgent return task \ No newline at end of file diff --git a/capmonstercloud_client/requests/GeetestRequest.py b/capmonstercloud_client/requests/GeetestRequest.py index b4bbf87..3dbb4e9 100644 --- a/capmonstercloud_client/requests/GeetestRequest.py +++ b/capmonstercloud_client/requests/GeetestRequest.py @@ -5,15 +5,35 @@ class GeetestRequest(BaseRequestWithProxy): - type: str = Field(default='GeeTestTask') - websiteUrl: str - gt: str - challenge: Optional[str] = Field(default=None) - version: int = Field(default=3) - initParameters: Optional[Dict] = Field(default=None) - geetestApiServerSubdomain: Optional[str] = Field(default=None) - geetestGetLib: Optional[str] = Field(default=None) - user_agent: Optional[str] = Field(default=None) + """ + Represents a payload structure for solving GeeTest challenges. + + Attributes: + type: The constant string value identifying the task type as "GeeTestTask". + websiteUrl: The URL of the webpage containing the GeeTest challenge. + gt: The public GeeTest key retrieved from the target website, identifying + the GeeTest account/challenge configuration. + challenge: A one-time token generated by the target website for each + challenge attempt. Required when solving GeeTest version 3. + version: The GeeTest challenge version to solve, either 3 or 4. + initParameters: Additional initialization parameters used by GeeTest + version 4 challenges, if the website requires them. + geetestApiServerSubdomain: The custom API server subdomain used by the + target website's GeeTest widget, if it deviates from the default. + geetestGetLib: Path to the captcha script used to display it on the + page, sent as a JSON string. Optional; may be required for some sites. + user_agent: Browser User-Agent to emulate. Pass only a current + Windows OS UA. + """ + type: str = Field(default='GeeTestTask', description='The constant string value identifying the task type as "GeeTestTask".') + websiteUrl: str = Field(..., description='The URL of the webpage containing the GeeTest challenge.') + gt: str = Field(..., description='The public GeeTest key retrieved from the target website, identifying the GeeTest account/challenge configuration.') + challenge: Optional[str] = Field(default=None, description='A one-time token generated by the target website for each challenge attempt. Required when solving GeeTest version 3.') + version: int = Field(default=3, description='The GeeTest challenge version to solve, either 3 or 4.') + initParameters: Optional[Dict] = Field(default=None, description="Additional initialization parameters used by GeeTest version 4 challenges, if the website requires them.") + geetestApiServerSubdomain: Optional[str] = Field(default=None, description="The custom API server subdomain used by the target website's GeeTest widget, if it deviates from the default.") + geetestGetLib: Optional[str] = Field(default=None, description='Path to the captcha script used to display it on the page, sent as a JSON string. Optional; may be required for some sites.') + user_agent: Optional[str] = Field(default=None, description='Browser User-Agent to emulate. Pass only a current Windows OS UA.') @field_validator('version') @classmethod diff --git a/capmonstercloud_client/requests/HcaptchaComplexImageTask.py b/capmonstercloud_client/requests/HcaptchaComplexImageTask.py index 3c8f88b..68783e0 100644 --- a/capmonstercloud_client/requests/HcaptchaComplexImageTask.py +++ b/capmonstercloud_client/requests/HcaptchaComplexImageTask.py @@ -5,11 +5,31 @@ from ..exceptions import NumbersImagesErrors, ZeroImagesErrors, TaskNotDefinedError, ExtraParamsError class HcaptchaComplexImageTaskRequest(ComplexImageTaskRequestBase): - - captchaClass: str = Field(default='hcaptcha') - metadata : Dict[str, str] - exampleImageUrls: Optional[List[str]] = None - exampleImagesBase64: Optional[List[str]] = None + """ + Represents a payload structure for solving hCaptcha "complex image" + challenges, where a worker must select images matching a described + object or example image across a grid of candidate images. + + Attributes: + captchaClass: The constant string value identifying the hCaptcha + challenge class, used to select the correct solving logic. + metadata: A dictionary describing the challenge, including the + mandatory "Task" key with the instruction shown to the worker. + exampleImageUrls: A list of URLs pointing to example image(s) + that illustrate the object to be found. Mutually exclusive + with exampleImagesBase64. + exampleImagesBase64: A list of base64-encoded example image(s) + that illustrate the object to be found. Mutually exclusive + with exampleImageUrls. + imagesUrls: A collection of image URLs to be recognized (up to 18). + Must be populated if imagesBase64 is not. + """ + + captchaClass: str = Field(default='hcaptcha', description='The constant string value identifying the hCaptcha challenge class.') + metadata : Dict[str, str] = Field(..., description='Dictionary describing the challenge, must contain a "Task" key with the instruction for the worker.') + exampleImageUrls: Optional[List[str]] = Field(default=None, description='List of URLs of example image(s) illustrating the object to find. Mutually exclusive with exampleImagesBase64.') + exampleImagesBase64: Optional[List[str]] = Field(default=None, description='List of base64-encoded example image(s) illustrating the object to find. Mutually exclusive with exampleImageUrls.') + imagesUrls: Optional[List[str]] = Field(default=None, description='Collection with image urls (up to 18). Must be populated if imagesBase64 is not.') @staticmethod def _validate_image_array(value, field_name, max_images): diff --git a/capmonstercloud_client/requests/HcaptchaRequest.py b/capmonstercloud_client/requests/HcaptchaRequest.py index e990cd7..bb4f70f 100644 --- a/capmonstercloud_client/requests/HcaptchaRequest.py +++ b/capmonstercloud_client/requests/HcaptchaRequest.py @@ -4,14 +4,32 @@ from .baseRequestWithProxy import BaseRequestWithProxy class HcaptchaRequest(BaseRequestWithProxy): - type: str = Field(default='HCaptchaTask') - websiteUrl: str - websiteKey: str - is_invisible: Optional[bool] = Field(default=None) - data: Optional[str] = Field(default=None) - user_agent: Optional[str] = Field(default=None) - cookies: Optional[str] = Field(default=None) - fallbackToActualUA: Optional[bool] = Field(default=None) + """ + Represents a payload structure for solving hCaptcha challenges. + + Attributes: + type: The constant string value identifying the task type as "HCaptchaTask". + websiteUrl: The URL of the webpage containing the hCaptcha challenge. + websiteKey: The site key associated with the hCaptcha on the webpage. + is_invisible: When set to True, specifies that the challenge + being solved is an invisible hCaptcha. + data: The value of the optional "data" attribute used by some + hCaptcha implementations, if present on the target page. + user_agent: The User-Agent string of the browser to associate + with the solving session. + cookies: Cookies to be used during the solving process, formatted + as a single string of "name1=value1; name2=value2" pairs. + fallbackToActualUA: When set to True, allows the worker's actual + User-Agent to be used as a fallback if a specific one is not required. + """ + type: str = Field(default='HCaptchaTask', description='The constant string value identifying the task type as "HCaptchaTask".') + websiteUrl: str = Field(..., description='The URL of the webpage containing the hCaptcha challenge.') + websiteKey: str = Field(..., description='The site key associated with the hCaptcha on the webpage.') + is_invisible: Optional[bool] = Field(default=None, description='When set to True, specifies that the challenge being solved is an invisible hCaptcha.') + data: Optional[str] = Field(default=None, description='The value of the optional "data" attribute used by some hCaptcha implementations, if present on the target page.') + user_agent: Optional[str] = Field(default=None, description='The User-Agent string of the browser to associate with the solving session.') + cookies: Optional[str] = Field(default=None, description='Cookies to be used during the solving process, formatted as a single string of "name1=value1; name2=value2" pairs.') + fallbackToActualUA: Optional[bool] = Field(default=None, description="When set to True, allows the worker's actual User-Agent to be used as a fallback if a specific one is not required.") def getTaskDict(self) -> Dict[str, Union[str, int, bool]]: diff --git a/capmonstercloud_client/requests/HuntCustomTaskRequest.py b/capmonstercloud_client/requests/HuntCustomTaskRequest.py index ba08143..b313f59 100644 --- a/capmonstercloud_client/requests/HuntCustomTaskRequest.py +++ b/capmonstercloud_client/requests/HuntCustomTaskRequest.py @@ -4,8 +4,21 @@ from .CustomTaskRequestBase import CustomTaskRequestBase class HuntCustomTaskRequest(CustomTaskRequestBase): - captchaClass: str = Field(default='HUNT') - metadata: Dict[str, str] + """ + Represents a payload structure for solving custom HUNT challenges. + + Attributes: + captchaClass: The constant string value identifying the underlying + captcha class as "HUNT". + metadata: A dictionary of parameters required by the HUNT solver. + Always requires "apiGetLib" (the URL of the HUNT JS script on + the page). HUNT has two solving modes: fingerprint generation + (only "apiGetLib" needed) and captcha solving (also requires + "data", which must hold the "meta.token" value extracted from + the page). + """ + captchaClass: str = Field(default='HUNT', description='The constant string value identifying the underlying captcha class as "HUNT".') + metadata: Dict[str, str] = Field(..., description='Dictionary of HUNT parameters. Always requires "apiGetLib". Also requires "data" (the "meta.token" value from the page) when solving a captcha rather than just generating a fingerprint.') @field_validator('metadata') @classmethod diff --git a/capmonstercloud_client/requests/ImageToTextRequest.py b/capmonstercloud_client/requests/ImageToTextRequest.py index 4a3b468..6a84fb4 100644 --- a/capmonstercloud_client/requests/ImageToTextRequest.py +++ b/capmonstercloud_client/requests/ImageToTextRequest.py @@ -6,21 +6,41 @@ from .enums import TextModules class ImageToTextRequest(BaseRequest): + """ + Represents a payload structure for solving image-based text recognition + (ImageToText) captchas. - image_bytes: bytes - type: str = "ImageToTextTask" - module_name: Optional[str] = Field(default=None) - threshold: Optional[int] = Field(default=None) - case: Optional[bool] = Field(default=None) - numeric: Optional[int] = Field(default=None) - math: Optional[bool] = Field(default=None) + Attributes: + image_bytes: The raw bytes of the image containing the text to be + recognized. It is base64-encoded before being sent to the API. + type: The constant string value identifying the task type as "ImageToTextTask". + module_name: The name of a specific CapMonster recognition module + to use for this image, tailored to a particular captcha service's format. + threshold: The minimum acceptable recognition confidence, expressed + as an integer between 0 and 100. + case: When set to True, specifies that the recognized text is + case-sensitive. + numeric: Restricts recognition to numeric characters only. Must be + either 0 (disabled) or 1 (enabled). + math: When set to True, specifies that the image contains a + mathematical expression that must be calculated rather than + transcribed. + """ + + image_bytes: bytes = Field(..., description='Raw bytes of the image containing the text to recognize.') + type: str = Field(default="ImageToTextTask", description='The constant string value identifying the task type as "ImageToTextTask".') + module_name: Optional[str] = Field(default=None, description='Name of a specific CapMonster recognition module to use for this image.') + threshold: Optional[int] = Field(default=None, description='Minimum acceptable recognition confidence, between 0 and 100.') + case: Optional[bool] = Field(default=None, description='When True, indicates that the recognized text is case-sensitive.') + numeric: Optional[int] = Field(default=None, description='Restricts recognition to numeric characters only; must be 0 or 1.') + math: Optional[bool] = Field(default=None, description='When True, indicates that the image contains a mathematical expression to calculate.') @field_validator('threshold') @classmethod def validate_threshold(cls, value): if value is not None: if value not in range(0, 101): - raise ValueError(f"threshold must be between 1 and 100, got {value}") + raise ValueError(f"threshold must be between 0 and 100, got {value}") return value @field_validator('module_name') diff --git a/capmonstercloud_client/requests/ImpervaCustomTaskRequest.py b/capmonstercloud_client/requests/ImpervaCustomTaskRequest.py index 86f746d..561c860 100644 --- a/capmonstercloud_client/requests/ImpervaCustomTaskRequest.py +++ b/capmonstercloud_client/requests/ImpervaCustomTaskRequest.py @@ -4,8 +4,20 @@ from .CustomTaskRequestBase import CustomTaskRequestBase class ImpervaCustomTaskRequest(CustomTaskRequestBase): - captchaClass: str = Field(default='Imperva') - metadata : Dict[str, str] + """ + Represents a payload structure for solving Imperva/Incapsula custom challenges. + + Attributes: + captchaClass: The constant string value identifying the captcha class + as "Imperva". + metadata: A dictionary of Imperva-specific parameters, including the + required incapsulaScriptUrl and incapsulaCookies values, and the + optional reese84UrlEndpoint value. + proxy: Proxy settings to route the request through. Required for + this task type — Imperva will not solve without your own proxy. + """ + captchaClass: str = Field(default='Imperva', description='The constant string value identifying the captcha class as "Imperva".') + metadata : Dict[str, str] = Field(..., description='A dictionary of Imperva-specific parameters, including the required incapsulaScriptUrl and incapsulaCookies values, and the optional reese84UrlEndpoint value.') @field_validator('metadata') @classmethod diff --git a/capmonstercloud_client/requests/MTCaptchaRequest.py b/capmonstercloud_client/requests/MTCaptchaRequest.py index e8f8e59..c999a1b 100644 --- a/capmonstercloud_client/requests/MTCaptchaRequest.py +++ b/capmonstercloud_client/requests/MTCaptchaRequest.py @@ -4,12 +4,27 @@ class MTCaptchaRequest(BaseRequestWithProxy): - type: str = Field(default="MTCaptchaTask") - websiteUrl: str - websiteKey: str - pageAction: Optional[str] = Field(default=None) - isInvisible: Optional[bool] = Field(default=None) - userAgent: Optional[str] = Field(default=None) + """ + Represents a payload structure for solving MTCaptcha challenges. + + Attributes: + type: The constant string value identifying the task type as "MTCaptchaTask". + websiteUrl: The URL of the webpage containing the MTCaptcha challenge. + websiteKey: The site key associated with the MTCaptcha widget on the webpage. + pageAction: An optional action name associated with the MTCaptcha + challenge, used by some site implementations. + isInvisible: When set to True, specifies that the challenge + being solved is an invisible MTCaptcha. + userAgent: Browser User-Agent to emulate. Pass only a current + Windows OS UA. + """ + + type: str = Field(default="MTCaptchaTask", description='The task type identifier, always "MTCaptchaTask".') + websiteUrl: str = Field(..., description='The URL of the webpage containing the MTCaptcha challenge.') + websiteKey: str = Field(..., description='The site key associated with the MTCaptcha widget on the webpage.') + pageAction: Optional[str] = Field(default=None, description='An optional action name associated with the MTCaptcha challenge.') + isInvisible: Optional[bool] = Field(default=None, description='Whether the MTCaptcha challenge is an invisible one.') + userAgent: Optional[str] = Field(default=None, description='Browser User-Agent to emulate. Pass only a current Windows OS UA.') def getTaskDict(self) -> Dict[str, Union[str, int, bool]]: task = {} diff --git a/capmonstercloud_client/requests/ProsopoTaskRequest.py b/capmonstercloud_client/requests/ProsopoTaskRequest.py index 943a96f..312424c 100644 --- a/capmonstercloud_client/requests/ProsopoTaskRequest.py +++ b/capmonstercloud_client/requests/ProsopoTaskRequest.py @@ -4,9 +4,18 @@ class ProsopoTaskRequest(BaseRequestWithProxy): - type: str = Field(default="ProsopoTask") - websiteUrl: str - websiteKey: str + """ + Represents a payload structure for solving Prosopo Procaptcha challenges. + + Attributes: + type: The constant string value identifying the task type as "ProsopoTask". + websiteUrl: The URL of the webpage containing the Prosopo challenge. + websiteKey: The site key associated with the Prosopo captcha on the webpage. + """ + + type: str = Field(default="ProsopoTask", description='The constant string value identifying the task type as "ProsopoTask".') + websiteUrl: str = Field(..., description='The URL of the webpage containing the Prosopo challenge.') + websiteKey: str = Field(..., description='The site key associated with the Prosopo captcha on the webpage.') def getTaskDict(self) -> Dict[str, Union[str, int, bool]]: task = {} diff --git a/capmonstercloud_client/requests/RecaptchaComplexImageTask.py b/capmonstercloud_client/requests/RecaptchaComplexImageTask.py index 0b7e878..52a6880 100644 --- a/capmonstercloud_client/requests/RecaptchaComplexImageTask.py +++ b/capmonstercloud_client/requests/RecaptchaComplexImageTask.py @@ -1,19 +1,35 @@ -from typing import Dict, Union +from typing import Dict, List, Optional, Union from pydantic import Field, field_validator from .ComplexImageTaskBase import ComplexImageTaskRequestBase from ..exceptions import NumbersImagesErrors, ZeroImagesErrors, TaskNotDefinedError class RecaptchaComplexImageTaskRequest(ComplexImageTaskRequestBase): - - metadata : Dict[str, str] - captchaClass: str = Field(default='recaptcha') - + """ + Represents a payload structure for solving reCAPTCHA-style grid-based + image classification challenges. + + Attributes: + metadata: A dictionary describing the challenge. Must contain + "Task" (English task name, e.g. "Click on traffic lights") and + "TaskDefinition" (its technical identifier, e.g. "/m/015qff") — + both required together — along with "Grid" (e.g. "3x3", "4x4", + "1x1") specifying the image grid layout. + captchaClass: The constant string identifying the captcha family + as "recaptcha". + imagesUrls: A collection of image URLs to be recognized. Must be + populated if imagesBase64 is not. + """ + + metadata : Dict[str, str] = Field(..., description='Dictionary describing the challenge. Must contain "Task" and "TaskDefinition" (both required together) plus "Grid".') + captchaClass: str = Field(default='recaptcha', description='Constant string identifying the captcha family as "recaptcha".') + imagesUrls: Optional[List[str]] = Field(default=None, description='Collection with image urls. Must be populated if imagesBase64 is not.') + @field_validator('metadata') @classmethod def validate_metadata(cls, value): - if value.get('Task') is None and value.get('TaskDefinition') is None: - raise TaskNotDefinedError(f'Expect at least one of value(Task or TaskDefinition) will be filled.') + if value.get('Task') is None or value.get('TaskDefinition') is None: + raise TaskNotDefinedError(f'Expect that both "Task" and "TaskDefinition" will be filled.') elif value.get('Grid') is None: raise TaskNotDefinedError(f'Expect that "Grid" value will be filled(3x3, 4x4, 1x1).') else: diff --git a/capmonstercloud_client/requests/RecaptchaV2EnterpiseRequest.py b/capmonstercloud_client/requests/RecaptchaV2EnterpiseRequest.py index 845297a..559b4f2 100644 --- a/capmonstercloud_client/requests/RecaptchaV2EnterpiseRequest.py +++ b/capmonstercloud_client/requests/RecaptchaV2EnterpiseRequest.py @@ -4,14 +4,40 @@ from .baseRequestWithProxy import BaseRequestWithProxy class RecaptchaV2EnterpriseRequest(BaseRequestWithProxy): - type: str = Field(default='RecaptchaV2EnterpriseTask') - websiteUrl: str - websiteKey: str - enterprisePayload: Optional[str] = Field(default=None) - apiDomain: Optional[str] = Field(default=None) - pageAction: Optional[str] = Field(default=None) - userAgent: Optional[str] = Field(default=None) - cookies: Optional[str] = Field(default=None) + """ + Represents a payload structure for solving reCAPTCHA v2 Enterprise challenges. + + Attributes: + type: The constant string value identifying the task type as + "RecaptchaV2EnterpriseTask". + websiteUrl: The URL of the webpage containing the reCAPTCHA + Enterprise challenge. + websiteKey: The site key associated with the reCAPTCHA Enterprise + widget on the webpage. + enterprisePayload: Additional parameters passed to the reCAPTCHA + Enterprise widget, such as a custom "s" value used by some + implementations. + apiDomain: The domain to load the reCAPTCHA API from, used when the + challenge is served from a domain other than the default Google + domain. + pageAction: The action name configured for the reCAPTCHA Enterprise + challenge on the target website. + recaptchaDataSValue: The one-time token from the widget's "data-s" + parameter, if present on the page. + userAgent: The User-Agent string of the browser that should be + emulated while solving the challenge. + cookies: Cookies to be sent along with the request when solving the + challenge. + """ + type: str = Field(default='RecaptchaV2EnterpriseTask', description='The constant string value identifying the task type as "RecaptchaV2EnterpriseTask".') + websiteUrl: str = Field(..., description='The URL of the webpage containing the reCAPTCHA Enterprise challenge.') + websiteKey: str = Field(..., description='The site key associated with the reCAPTCHA Enterprise widget on the webpage.') + enterprisePayload: Optional[str] = Field(default=None, description='Additional parameters passed to the reCAPTCHA Enterprise widget, such as a custom "s" value used by some implementations.') + apiDomain: Optional[str] = Field(default=None, description='The domain to load the reCAPTCHA API from, used when the challenge is served from a domain other than the default Google domain.') + pageAction: Optional[str] = Field(default=None, description='The action name configured for the reCAPTCHA Enterprise challenge on the target website.') + recaptchaDataSValue: Optional[str] = Field(default=None, description='The one-time token from the widget\'s "data-s" parameter, if present on the page.') + userAgent: Optional[str] = Field(default=None, description='The User-Agent string of the browser that should be emulated while solving the challenge.') + cookies: Optional[str] = Field(default=None, description='Cookies to be sent along with the request when solving the challenge.') def getTaskDict(self) -> Dict[str, Union[str, int]]: task = {} @@ -30,6 +56,8 @@ def getTaskDict(self) -> Dict[str, Union[str, int]]: task['apiDomain'] = self.apiDomain if self.pageAction is not None: task['pageAction'] = self.pageAction + if self.recaptchaDataSValue is not None: + task['recaptchaDataSValue'] = self.recaptchaDataSValue if self.userAgent is not None: task['userAgent'] = self.userAgent if self.cookies is not None: diff --git a/capmonstercloud_client/requests/RecaptchaV2Request.py b/capmonstercloud_client/requests/RecaptchaV2Request.py index 34bfb91..92de4b2 100644 --- a/capmonstercloud_client/requests/RecaptchaV2Request.py +++ b/capmonstercloud_client/requests/RecaptchaV2Request.py @@ -4,14 +4,33 @@ class RecaptchaV2Request(BaseRequestWithProxy): - - type: str = Field(default="NoCaptchaTask") - websiteUrl: str - websiteKey: str - dataSValue: Optional[str] = Field(default=None) - userAgent: Optional[str] = Field(default=None) - cookies: Optional[str] = Field(default=None) - isInvisible: Optional[bool] = Field(default=None) + """ + Represents a request payload for solving reCAPTCHA v2 challenges. + + Attributes: + type: The constant string value identifying the task type as "RecaptchaV2Task". + websiteUrl: The URL of the webpage containing the reCAPTCHA challenge. + websiteKey: The site key associated with the reCAPTCHA on the webpage. + dataSValue: A one-time token specific to certain custom + implementations of reCAPTCHA v2. If applicable, this parameter needs + to be retrieved for each challenge-solving attempt. + userAgent: The User-Agent header of the browser to be emulated + while solving the captcha. Must be a current/modern browser + signature — a stale or non-standard UA causes Google to return + an "update your browser" error instead of solving the captcha. + cookies: Cookies to be used when accessing the target webpage + while solving the captcha, formatted as "name1=value1; name2=value2". + isInvisible: When set to True, specifies that the challenge + being solved is an invisible reCAPTCHA. + """ + + type: str = Field(default="RecaptchaV2Task", description='The constant string value identifying the task type as "RecaptchaV2Task".') + websiteUrl: str = Field(..., description='The URL of the webpage containing the reCAPTCHA challenge.') + websiteKey: str = Field(..., description='The site key associated with the reCAPTCHA on the webpage.') + dataSValue: Optional[str] = Field(default=None, description='A one-time token specific to certain custom implementations of reCAPTCHA v2, retrieved per challenge-solving attempt.') + userAgent: Optional[str] = Field(default=None, description='The User-Agent header of the browser to be emulated while solving the captcha. Must be a current/modern browser signature, otherwise Google will return an error asking for a browser update.') + cookies: Optional[str] = Field(default=None, description='Cookies to be used when accessing the target webpage while solving the captcha, formatted as "name1=value1; name2=value2".') + isInvisible: Optional[bool] = Field(default=None, description='When set to True, specifies that the challenge being solved is an invisible reCAPTCHA.') def getTaskDict(self) -> Dict[str, Union[str, int, bool]]: task = {} diff --git a/capmonstercloud_client/requests/RecaptchaV3EnterpriseRequest.py b/capmonstercloud_client/requests/RecaptchaV3EnterpriseRequest.py index 3621abf..d3b9440 100644 --- a/capmonstercloud_client/requests/RecaptchaV3EnterpriseRequest.py +++ b/capmonstercloud_client/requests/RecaptchaV3EnterpriseRequest.py @@ -4,11 +4,25 @@ from .baseRequest import BaseRequest class RecaptchaV3EnterpriseRequest(BaseRequest): - type: str = Field(default='RecaptchaV3EnterpriseTask') - websiteUrl: str - websiteKey: str - minScore: Optional[float] = Field(default=None) - pageAction: Optional[str] = Field(default=None) + """ + Represents a payload structure for solving reCAPTCHA v3 Enterprise challenges. + + Attributes: + type: The constant string value identifying the task type as + "RecaptchaV3EnterpriseTask". + websiteUrl: The URL of the webpage containing the reCAPTCHA v3 + Enterprise challenge. + websiteKey: The site key associated with the reCAPTCHA on the webpage. + minScore: Minimum acceptable score for the token, in the range + 0.1-0.9. The returned token will be valid for this score threshold. + pageAction: The action name configured for this reCAPTCHA v3 + Enterprise widget on the target website. + """ + type: str = Field(default='RecaptchaV3EnterpriseTask', description='The constant string value identifying the task type as "RecaptchaV3EnterpriseTask".') + websiteUrl: str = Field(..., description='The URL of the webpage containing the reCAPTCHA v3 Enterprise challenge.') + websiteKey: str = Field(..., description='The site key associated with the reCAPTCHA on the webpage.') + minScore: Optional[float] = Field(default=None, description='Minimum acceptable score for the token, in the range 0.1-0.9.') + pageAction: Optional[str] = Field(default=None, description='The action name configured for this reCAPTCHA v3 Enterprise widget on the target website.') @field_validator('minScore') @classmethod diff --git a/capmonstercloud_client/requests/RecaptchaV3ProxylessRequest.py b/capmonstercloud_client/requests/RecaptchaV3ProxylessRequest.py index 2c564d0..070387a 100644 --- a/capmonstercloud_client/requests/RecaptchaV3ProxylessRequest.py +++ b/capmonstercloud_client/requests/RecaptchaV3ProxylessRequest.py @@ -4,11 +4,28 @@ class RecaptchaV3ProxylessRequest(BaseRequest): - websiteUrl: str - websiteKey: str - type: str = Field(default='RecaptchaV3TaskProxyless') - minScore: Optional[float] = Field(default=None) - pageAction: Optional[str] = Field(default=None) + """ + Represents a payload structure for solving reCAPTCHA v3 challenges without a proxy. + + Attributes: + websiteUrl: The URL of the webpage containing the reCAPTCHA v3 challenge. + websiteKey: The site key associated with the reCAPTCHA on the webpage. + type: The constant string value identifying the task type as + "RecaptchaV3TaskProxyless". + minScore: The minimum acceptable score for the returned token, in the + range 0.1-0.9. Higher values request a token that appears more + human-like to the target website. + pageAction: The action name configured on the webpage for this + reCAPTCHA v3 challenge, used to influence the score calculation. + isEnterprise: Set to True to solve this reCAPTCHA v3 challenge via + the Enterprise solver, using this same proxyless task type. + """ + websiteUrl: str = Field(..., description='Address of a webpage with reCAPTCHA v3.') + websiteKey: str = Field(..., description='The site key associated with the reCAPTCHA on the webpage.') + type: str = Field(default='RecaptchaV3TaskProxyless', description='The constant string value identifying the task type as "RecaptchaV3TaskProxyless".') + minScore: Optional[float] = Field(default=None, description='Minimum acceptable score for the returned token (0.1-0.9).') + pageAction: Optional[str] = Field(default=None, description='Action name configured on the webpage for the reCAPTCHA v3 challenge.') + isEnterprise: Optional[bool] = Field(default=None, description='Set to True to solve this reCAPTCHA v3 challenge via the Enterprise solver, using this same proxyless task type.') @field_validator('minScore') @classmethod @@ -28,4 +45,6 @@ def getTaskDict(self) -> Dict[str, Union[str, float]]: task['minScore'] = self.minScore if self.pageAction is not None: task['pageAction'] = self.pageAction + if self.isEnterprise is not None: + task['isEnterprise'] = self.isEnterprise return task \ No newline at end of file diff --git a/capmonstercloud_client/requests/RecognitionComplexImageTaskRequest.py b/capmonstercloud_client/requests/RecognitionComplexImageTaskRequest.py index ea9b865..3e37306 100644 --- a/capmonstercloud_client/requests/RecognitionComplexImageTaskRequest.py +++ b/capmonstercloud_client/requests/RecognitionComplexImageTaskRequest.py @@ -1,12 +1,25 @@ from typing import Dict, Union -from pydantic import field_validator +from pydantic import Field, field_validator from .ComplexImageTaskBase import ComplexImageTaskRequestBase from ..exceptions import ZeroImagesErrors, TaskNotDefinedError class RecognitionComplexImageTaskRequest(ComplexImageTaskRequestBase): - captchaClass: str = 'recognition' - metadata: Dict[str, str] + """ + Represents a payload structure for solving recognition-type complex + image captchas, where images must be classified or matched against a + described task. + + Attributes: + captchaClass: The class (subtype) of the ComplexImageTask, fixed to + "recognition" for this task type. + metadata: Must contain a "Task" key naming the CapMonster recognition + module to run (e.g. "shein", "bls_3x3", "bills_audio"), and may + optionally contain a "TaskArgument" key carrying the target/instruction + for that module (e.g. a number to match or an icon name). + """ + captchaClass: str = Field(default='recognition', description='Class(subtype) of ComplexImageTask, fixed to "recognition".') + metadata: Dict[str, str] = Field(..., description='Must contain a "Task" key naming the CapMonster recognition module to run (e.g. "shein", "bls_3x3"), and may optionally contain a "TaskArgument" key carrying the target/instruction for that module.') @field_validator('metadata') @classmethod diff --git a/capmonstercloud_client/requests/TemuCustomTaskRequest.py b/capmonstercloud_client/requests/TemuCustomTaskRequest.py index 8b5ed7b..b8ac80c 100644 --- a/capmonstercloud_client/requests/TemuCustomTaskRequest.py +++ b/capmonstercloud_client/requests/TemuCustomTaskRequest.py @@ -4,8 +4,20 @@ from .CustomTaskRequestBase import CustomTaskRequestBase class TemuCustomTaskRequest(CustomTaskRequestBase): - captchaClass: str = Field(default='Temu') - metadata: Dict[str, str] + """ + Represents a payload structure for solving Temu's custom captcha + challenge. + + Attributes: + captchaClass: The constant string value identifying the custom + module class as "Temu". + metadata: A dictionary carrying the additional data required to + solve the captcha; must contain a "cookie" string value and no + other keys. + """ + + captchaClass: str = Field(default='Temu', description='Class (subtype) identifier of the custom module, constant "Temu".') + metadata: Dict[str, str] = Field(..., description='Additional data required to solve the captcha; must contain a "cookie" string value and no other keys.') @field_validator('metadata') @classmethod diff --git a/capmonstercloud_client/requests/TenDiCustomTaskRequest.py b/capmonstercloud_client/requests/TenDiCustomTaskRequest.py index ca0bed5..d96bada 100644 --- a/capmonstercloud_client/requests/TenDiCustomTaskRequest.py +++ b/capmonstercloud_client/requests/TenDiCustomTaskRequest.py @@ -4,9 +4,20 @@ from .CustomTaskRequestBase import CustomTaskRequestBase class TenDiCustomTaskRequest(CustomTaskRequestBase): - captchaClass: str = Field(default='TenDI') - websiteKey: str = Field() - metadata: Optional[Dict[str, str]] = Field(default=None) + """ + Represents a payload structure for solving TenDI custom captcha challenges. + + Attributes: + captchaClass: The identifier of the custom captcha class to solve, + defaulting to "TenDI". + websiteKey: The captchaAppId (the "aid" value, e.g. "189123456") for + the target website, found in the page HTML or network traffic. + metadata: Optional additional parameters for the task. Currently + supports the "captchaUrl" key, whose value must be a string. + """ + captchaClass: str = Field(default='TenDI', description='The identifier of the custom captcha class to solve.') + websiteKey: str = Field(description='The captchaAppId (the "aid" value, e.g. "189123456") for the target website, found in the page HTML or network traffic.') + metadata: Optional[Dict[str, str]] = Field(default=None, description='Optional additional parameters for the task; supports the "captchaUrl" key with a string value.') @field_validator('metadata') @classmethod diff --git a/capmonstercloud_client/requests/TspdCustomTaskRequest.py b/capmonstercloud_client/requests/TspdCustomTaskRequest.py index 4018f4b..a2fc067 100644 --- a/capmonstercloud_client/requests/TspdCustomTaskRequest.py +++ b/capmonstercloud_client/requests/TspdCustomTaskRequest.py @@ -4,9 +4,24 @@ from .CustomTaskRequestBase import CustomTaskRequestBase class TspdCustomTaskRequest(CustomTaskRequestBase): - captchaClass: str = Field(default='tspd') - userAgent: str = Field() - metadata: Dict[str, str] + """ + Represents a payload structure for solving TSPD (PerimeterX/HUMAN + Bot Defender) custom challenges. + + Attributes: + captchaClass: The class (subtype) identifier of the custom module, + fixed to "tspd" for this task type. + userAgent: Browser User-Agent to emulate. Pass only a current + Windows OS UA, matching the one used when the tspdCookie and + htmlPageBase64 were obtained. + metadata: A dictionary carrying the challenge data required to solve + the TSPD task, including the "tspdCookie" string and the + "htmlPageBase64" string. + """ + + captchaClass: str = Field(default='tspd', description='Class (subtype) identifier of the custom module, fixed to "tspd".') + userAgent: str = Field(description='Browser User-Agent to emulate. Pass only a current Windows OS UA, matching the one used when the tspdCookie and htmlPageBase64 were obtained.') + metadata: Dict[str, str] = Field(description='Challenge data required to solve the task, including "tspdCookie" and "htmlPageBase64".') @field_validator('metadata') @classmethod diff --git a/capmonstercloud_client/requests/TurnstileRequest.py b/capmonstercloud_client/requests/TurnstileRequest.py index 3851961..63b8c04 100644 --- a/capmonstercloud_client/requests/TurnstileRequest.py +++ b/capmonstercloud_client/requests/TurnstileRequest.py @@ -4,17 +4,42 @@ class TurnstileRequest(BaseRequestWithProxy): - - type: str = Field(default="TurnstileTask") - websiteURL: str - websiteKey: str - pageAction: Optional[str] = Field(default=None) - data: Optional[str] = Field(default=None) - pageData: Optional[str] = Field(default=None) - userAgent: Optional[str] = Field(default=None) - cloudflareTaskType: Optional[str] = Field(default=None) - htmlPageBase64: Optional[str] = Field(default=None) - apiJsUrl: Optional[str] = Field(default=None) + """ + Represents a payload structure for solving Cloudflare Turnstile challenges. + + Attributes: + type: The constant string value identifying the task type as "TurnstileTask". + websiteURL: The URL of the webpage containing the Turnstile challenge. + websiteKey: The site key associated with the Turnstile widget on the webpage. + pageAction: The action name configured for the Turnstile widget, if the + website uses the "action" parameter to distinguish between challenges. + Required when cloudflareTaskType is "token". + data: The "cData" custom payload value passed to the Turnstile widget, + required when cloudflareTaskType is "token" and the widget uses this field. + pageData: The "chlPageData" value used by the Turnstile widget for + interactive challenges, required when cloudflareTaskType is "token" and + the widget uses this field. + userAgent: The User-Agent string of the browser to emulate. Required + whenever cloudflareTaskType is specified. + cloudflareTaskType: The mode of the Cloudflare task: "cf_clearance" to + obtain the cf_clearance cookie, "token" to obtain a Turnstile token, or + "wait_room" to pass a Cloudflare waiting room. + htmlPageBase64: The base64-encoded HTML source of the target page. + Required when cloudflareTaskType is "cf_clearance" or "wait_room". + apiJsUrl: The URL of the Turnstile API script (api.js) used on the page, + if it differs from the default Cloudflare endpoint. + """ + + type: str = Field(default="TurnstileTask", description='The constant string value identifying the task type as "TurnstileTask".') + websiteURL: str = Field(..., description='The URL of the webpage containing the Turnstile challenge.') + websiteKey: str = Field(..., description='The site key associated with the Turnstile widget on the webpage.') + pageAction: Optional[str] = Field(default=None, description='The action name configured for the Turnstile widget, if the website uses the "action" parameter to distinguish between challenges. Required when cloudflareTaskType is "token".') + data: Optional[str] = Field(default=None, description='The "cData" custom payload value passed to the Turnstile widget, required when cloudflareTaskType is "token" and the widget uses this field.') + pageData: Optional[str] = Field(default=None, description='The "chlPageData" value used by the Turnstile widget for interactive challenges, required when cloudflareTaskType is "token" and the widget uses this field.') + userAgent: Optional[str] = Field(default=None, description='The User-Agent string of the browser to emulate. Required whenever cloudflareTaskType is specified.') + cloudflareTaskType: Optional[str] = Field(default=None, description='The mode of the Cloudflare task: "cf_clearance" to obtain the cf_clearance cookie, "token" to obtain a Turnstile token, or "wait_room" to pass a Cloudflare waiting room.') + htmlPageBase64: Optional[str] = Field(default=None, description='The base64-encoded HTML source of the target page. Required when cloudflareTaskType is "cf_clearance" or "wait_room".') + apiJsUrl: Optional[str] = Field(default=None, description='The URL of the Turnstile API script (api.js) used on the page, if it differs from the default Cloudflare endpoint.') @field_validator('cloudflareTaskType') @classmethod diff --git a/capmonstercloud_client/requests/YidunRequest.py b/capmonstercloud_client/requests/YidunRequest.py index dcc05c9..dd425b0 100644 --- a/capmonstercloud_client/requests/YidunRequest.py +++ b/capmonstercloud_client/requests/YidunRequest.py @@ -4,15 +4,38 @@ class YidunRequest(BaseRequestWithProxy): - type: str = Field(default="YidunTask") - websiteUrl: str - websiteKey: str - userAgent: Optional[str] = Field(default=None) - yidunGetLib: Optional[str] = Field(default=None) - yidunApiServerSubdomain: Optional[str] = Field(default=None) - challenge: Optional[str] = Field(default=None) - hcg: Optional[str] = Field(default=None) - hct: Optional[int] = Field(default=None) + """ + Represents a payload structure for solving NetEase Yidun captcha challenges. + + Attributes: + type: The constant string value identifying the task type as "YidunTask". + websiteUrl: The URL of the webpage containing the Yidun captcha challenge. + websiteKey: The siteKey value associated with the Yidun widget on the webpage. + userAgent: Browser User-Agent to emulate. Pass only a current + Windows OS UA. + yidunGetLib: The URL of the get lib script used by the Yidun widget, + extracted from the page source when the default one does not apply. + yidunApiServerSubdomain: The custom API server subdomain used by the + Yidun widget, if the target page overrides the default one. + challenge: Unique identifier of the current captcha. Its presence + indicates the Enterprise/Business Yidun variant, and it is + typically supplied together with hcg, hct, yidunGetLib, and + yidunApiServerSubdomain. + hcg: An additional Yidun-specific parameter (hcg) sometimes required + by the widget to complete verification. + hct: An additional Yidun-specific numeric parameter (hct) sometimes + required by the widget to complete verification. + """ + + type: str = Field(default="YidunTask", description='The constant string value identifying the task type as "YidunTask".') + websiteUrl: str = Field(..., description='The URL of the webpage containing the Yidun captcha challenge.') + websiteKey: str = Field(..., description='The siteKey value associated with the Yidun widget on the webpage.') + userAgent: Optional[str] = Field(default=None, description='Browser User-Agent to emulate. Pass only a current Windows OS UA.') + yidunGetLib: Optional[str] = Field(default=None, description='The URL of the get lib script used by the Yidun widget, extracted from the page source when the default one does not apply.') + yidunApiServerSubdomain: Optional[str] = Field(default=None, description='The custom API server subdomain used by the Yidun widget, if the target page overrides the default one.') + challenge: Optional[str] = Field(default=None, description='Unique identifier of the current captcha. Its presence indicates the Enterprise/Business Yidun variant, and it is typically supplied together with hcg, hct, yidunGetLib, and yidunApiServerSubdomain.') + hcg: Optional[str] = Field(default=None, description='An additional Yidun-specific parameter (hcg) sometimes required by the widget to complete verification.') + hct: Optional[int] = Field(default=None, description='An additional Yidun-specific numeric parameter (hct) sometimes required by the widget to complete verification.') def getTaskDict(self) -> Dict[str, Union[str, int, bool]]: task = {} diff --git a/capmonstercloud_client/requests/baseRequest.py b/capmonstercloud_client/requests/baseRequest.py index d3b21db..2c5effb 100644 --- a/capmonstercloud_client/requests/baseRequest.py +++ b/capmonstercloud_client/requests/baseRequest.py @@ -3,8 +3,20 @@ from abc import ABC, abstractmethod class BaseRequest(BaseModel, ABC): - no_cache: bool = Field(default=False) - type: str = Field(default='') + """ + Represents the base payload structure shared by all CapMonster Cloud task requests. + + Attributes: + no_cache: Intended to disable the use of previously cached solutions + and force a fresh solve attempt when True. Currently unused: no + subclass includes it in getTaskDict(), so it has no effect on the + request actually sent. + type: The constant string value identifying the task type. Subclasses + override this with their specific task type name. + """ + + no_cache: bool = Field(default=False, description='Intended to disable the use of previously cached solutions and force a fresh solve attempt when True. Currently unused: no subclass includes it in getTaskDict(), so it has no effect on the request actually sent.') + type: str = Field(default='', description='The constant string value identifying the task type.') @abstractmethod def getTaskDict(self) -> Dict[str, Union[str, int, bool]]: diff --git a/capmonstercloud_client/requests/baseRequestWithProxy.py b/capmonstercloud_client/requests/baseRequestWithProxy.py index d069005..50f68c7 100644 --- a/capmonstercloud_client/requests/baseRequestWithProxy.py +++ b/capmonstercloud_client/requests/baseRequestWithProxy.py @@ -4,4 +4,13 @@ from .proxy_info import ProxyInfo class BaseRequestWithProxy(BaseRequest): - proxy: Optional[ProxyInfo] = None + """ + Represents a base payload structure for tasks that are solved using a + proxy server rather than CapMonster Cloud's own IP pool. + + Attributes: + proxy: Proxy settings (type, address, port, and credentials) to route + the captcha-solving request through a specified proxy server. + """ + + proxy: Optional[ProxyInfo] = Field(default=None, description='Proxy settings to route the request through a specified proxy server.') diff --git a/capmonstercloud_client/requests/enums.py b/capmonstercloud_client/requests/enums.py index 9df7703..e7ef858 100644 --- a/capmonstercloud_client/requests/enums.py +++ b/capmonstercloud_client/requests/enums.py @@ -3,6 +3,10 @@ class BaseEnum(Enum): + """ + Base enum class providing convenience helpers shared by the + module-specific enumerations defined below. + """ @classmethod def list_values(cls) -> List[str]: @@ -14,6 +18,11 @@ def list_names(cls) -> List[str]: @unique class TextModules(BaseEnum): + """ + Enumerates the recognizable text-captcha module identifiers accepted + by CapMonster Cloud's ImageToTextTask, used to select service-specific + recognition logic for known text captcha providers. + """ amazon_captcha = 'amazon' botdetect_captcha = 'botdetect' facebook_captcha = 'facebook' @@ -34,6 +43,10 @@ class TextModules(BaseEnum): @unique class ProxyTypes(BaseEnum): + """ + Enumerates the proxy protocols supported when routing a captcha-solving + request through a proxy server. + """ http_proxy = 'http' https_proxy = 'https' socks4_proxy = 'socks4' diff --git a/capmonstercloud_client/requests/proxy_info.py b/capmonstercloud_client/requests/proxy_info.py index 9d89d13..3533211 100644 --- a/capmonstercloud_client/requests/proxy_info.py +++ b/capmonstercloud_client/requests/proxy_info.py @@ -1,13 +1,23 @@ -from pydantic import BaseModel, field_validator +from pydantic import BaseModel, Field, field_validator from .enums import ProxyTypes from typing import Optional class ProxyInfo(BaseModel): - proxyType: str - proxyAddress: str - proxyPort: int - proxyLogin: str - proxyPassword: str + """ + Represents the proxy connection details used to route a captcha-solving request. + + Attributes: + proxyType: The protocol of the proxy server: "http", "https", "socks4", or "socks5". + proxyAddress: The IPv4/IPv6 address or hostname of the proxy server. + proxyPort: The port number on which the proxy server accepts connections. + proxyLogin: The username used to authenticate with the proxy server. + proxyPassword: The password used to authenticate with the proxy server. + """ + proxyType: str = Field(..., description='The protocol of the proxy server: "http", "https", "socks4", or "socks5".') + proxyAddress: str = Field(..., description='The IPv4/IPv6 address or hostname of the proxy server.') + proxyPort: int = Field(..., description='The port number on which the proxy server accepts connections.') + proxyLogin: str = Field(..., description='The username used to authenticate with the proxy server.') + proxyPassword: str = Field(..., description='The password used to authenticate with the proxy server.') @field_validator('proxyType') @classmethod @@ -24,12 +34,25 @@ def validate_port(cls, value): return value class ClientProxyInfo(BaseModel): - proxyType: str - proxyAddress: str - proxyPort: int - proxyLogin: Optional[str] = None - proxyPassword: Optional[str] = None - + """ + Represents the proxy connection details supplied by the client, with optional + authentication credentials. + + Attributes: + proxyType: The protocol of the proxy server: "http", "https", "socks4", or "socks5". + proxyAddress: The IPv4/IPv6 address or hostname of the proxy server. + proxyPort: The port number on which the proxy server accepts connections. + proxyLogin: The username used to authenticate with the proxy server, + if authentication is required. + proxyPassword: The password used to authenticate with the proxy server, + if authentication is required. + """ + proxyType: str = Field(..., description='The protocol of the proxy server: "http", "https", "socks4", or "socks5".') + proxyAddress: str = Field(..., description='The IPv4/IPv6 address or hostname of the proxy server.') + proxyPort: int = Field(..., description='The port number on which the proxy server accepts connections.') + proxyLogin: Optional[str] = Field(default=None, description='The username used to authenticate with the proxy server, if authentication is required.') + proxyPassword: Optional[str] = Field(default=None, description='The password used to authenticate with the proxy server, if authentication is required.') + @field_validator('proxyType') @classmethod def validate_proxy_type(cls, value): diff --git a/test/datadome_test.py b/test/datadome_test.py index c8c2ba9..1ad0ae0 100644 --- a/test/datadome_test.py +++ b/test/datadome_test.py @@ -32,12 +32,9 @@ def testCaptchaInputTypes(self): metadataListUrl = DataDomeCustomTaskRequestTest.metadataExample.copy() metadataListUrl["captchaUrl"] = list(metadataListUrl["captchaUrl"]) - metadataListImage = DataDomeCustomTaskRequestTest.metadataExample.copy() - metadataListImage.pop("captchaUrl") - metadataListImage["htmlPageBase64"] = metadataListUrl["captchaUrl"] + metadataMissingUrl = DataDomeCustomTaskRequestTest.metadataExample.copy() + metadataMissingUrl.pop("captchaUrl") - metadataListExtra = DataDomeCustomTaskRequestTest.metadataExample.copy() - metadataListExtra["htmlPageBase64"] = metadataListExtra["captchaUrl"] with self.assertRaises(ValidationError): request = DataDomeCustomTaskRequest( websiteUrl=DataDomeCustomTaskRequestTest.websiteUrlExample, @@ -45,25 +42,17 @@ def testCaptchaInputTypes(self): proxy=self.proxy, ) - with self.assertRaises(ValidationError): - request = DataDomeCustomTaskRequest( - websiteUrl=DataDomeCustomTaskRequestTest.websiteUrlExample, - metadata=metadataListImage, - proxy=self.proxy, - ) - with self.assertRaises(TypeError): request = DataDomeCustomTaskRequest( websiteUrl=DataDomeCustomTaskRequestTest.websiteUrlExample, - metadata=metadataListExtra, + metadata=metadataMissingUrl, proxy=self.proxy, ) def testAllRequiredFieldsFilling(self): required_fields = ["class", "type", "websiteURL", "metadata", "proxyType", "proxyAddress", "proxyPort", "proxyLogin", "proxyPassword"] - metadata_fields = ["datadomeCookie"] - one_of_fields = [["captchaUrl", "htmlPageBase64"]] + metadata_fields = ["datadomeCookie", "captchaUrl"] request = DataDomeCustomTaskRequest( websiteUrl=DataDomeCustomTaskRequestTest.websiteUrlExample, metadata=DataDomeCustomTaskRequestTest.metadataExample, @@ -83,9 +72,6 @@ def testAllRequiredFieldsFilling(self): msg=f"Required field {i} not in {request_dict}", ) - for i in one_of_fields: - self.assertTrue(len(set(i).intersection(metadata_dict.keys())) == 1) - self.assertEqual(request_dict["class"], "DataDome") self.assertEqual(request_dict["type"], "CustomTask") diff --git a/test/requests_generation_test.py b/test/requests_generation_test.py index 1874431..d2890ed 100644 --- a/test/requests_generation_test.py +++ b/test/requests_generation_test.py @@ -17,7 +17,7 @@ def test_rcv2(self): "cookies", "isInvisible", ] - rc2_no_proxy_type = "NoCaptchaTask" + rc2_no_proxy_type = "RecaptchaV2Task" request = requests.RecaptchaV2Request( websiteUrl="some_url", websiteKey="sime_key", @@ -36,7 +36,7 @@ def test_rcv2(self): msg=f"Task type of ReCaptchaV2 not equal to {rc2_no_proxy_type}", ) - rc2_proxy_type = "NoCaptchaTask" + rc2_proxy_type = "RecaptchaV2Task" default_proxy_keys = default_keys + PROXY_LIST proxy = requests.ProxyInfo( proxyType="http", @@ -86,6 +86,22 @@ def test_rcv3(self): msg=f"Task type of ReCaptchaV3 not equal to {rc3_type}", ) + self.assertNotIn( + "isEnterprise", task, + msg="isEnterprise should not be sent when not explicitly set.", + ) + + enterprise_request = requests.RecaptchaV3ProxylessRequest( + websiteUrl="some_url", + websiteKey="some_key", + isEnterprise=True, + ) + enterprise_task = enterprise_request.getTaskDict() + self.assertTrue( + enterprise_task.get("isEnterprise") is True, + msg="isEnterprise=True should be included in the task dict.", + ) + # validate_min_score: valid boundary values for valid_score in [0.1, 0.5, 0.9]: req = requests.RecaptchaV3ProxylessRequest(