Skip to content

Commit 52d89ce

Browse files
authored
Merge pull request #32 from ZennoLab/grk717/RecognitionComplexImageTask
Grk717/recognition complex image task
2 parents 9c22486 + a839d45 commit 52d89ce

10 files changed

Lines changed: 190 additions & 10 deletions

capmonstercloud_client/CapMonsterCloudClient.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
((BasiliskCustomTaskRequest, BasiliskCustomTaskProxylessRequest), getBasiliskTimeouts),
3030
((AmazonWafRequest, AmazonWafProxylessRequest), getAmazonWafTimeouts),
3131
((BinanceTaskRequest, BinanceTaskProxylessRequest), getBinanceTimeouts),
32-
((ImpervaCustomTaskRequest, ImpervaCustomTaskProxylessRequest), getImpervaTimeouts)
32+
((ImpervaCustomTaskRequest, ImpervaCustomTaskProxylessRequest), getImpervaTimeouts),
33+
((RecognitionComplexImageTaskRequest), getCITTimeouts)
3334
)
3435

3536

@@ -82,7 +83,8 @@ async def solve_captcha(self, request: Union[RecaptchaV2EnterpriseProxylessReque
8283
BinanceTaskRequest,
8384
BinanceTaskProxylessRequest,
8485
ImpervaCustomTaskRequest,
85-
ImpervaCustomTaskProxylessRequest],
86+
ImpervaCustomTaskProxylessRequest,
87+
RecognitionComplexImageTaskRequest],
8688
) -> Dict[str, str]:
8789
'''
8890
Non-blocking method for captcha solving.

capmonstercloud_client/GetResultTimeouts.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,6 @@ def getBinanceTimeouts() -> GetResultTimeouts:
4949

5050
def getImpervaTimeouts() -> GetResultTimeouts:
5151
return GetResultTimeouts(1, 0, 1, 20)
52+
53+
def getCITTimeouts() -> GetResultTimeouts:
54+
return GetResultTimeouts(0.35, 0, 0.2, 10)

capmonstercloud_client/requests/RecaptchaComplexImageTask.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def getTaskDict(self) -> Dict[str, Union[str, int, bool]]:
7171

7272
if self.websiteUrl is not None:
7373
task["websiteUrl"] = self.websiteUrl
74-
74+
7575
return task
7676

7777

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from typing import Optional, List, Dict, Union
2+
from pydantic import validator
3+
from .ComplexImageTaskBase import ComplexImageTaskRequestBase
4+
from ..exceptions import NumbersImagesErrors, ZeroImagesErrors, TaskNotDefinedError
5+
6+
7+
class RecognitionComplexImageTaskRequest(ComplexImageTaskRequestBase):
8+
captchaClass: str = 'recognition'
9+
metadata: Dict[str, str]
10+
11+
@validator('metadata')
12+
def validate_metadata(cls, value):
13+
if value.get('Task') is None:
14+
raise TaskNotDefinedError(f'Expect that Task will be defined.')
15+
else:
16+
if not isinstance(value.get('Task'), str):
17+
raise TypeError(f'Expect that Task will be str.')
18+
if not set(value.keys()).issubset(set(["Task", "TaskArgument"])):
19+
raise TypeError(f'Allowed keys for metadata are "Task" and "TaskArgument"')
20+
return value
21+
22+
@validator('imagesBase64')
23+
def validate_images_array(cls, value):
24+
if value is not None:
25+
if not isinstance(value, (list, tuple)):
26+
raise TypeError(f'Expect that type imagesBase64 array will be <list> or <tuple>, got {type(value)}')
27+
elif not len(value):
28+
raise ZeroImagesErrors(f'At least one image base64 expected, got {len(value)}')
29+
# Check for each element type
30+
contain_types = [isinstance(x, str) for x in value]
31+
if not all(contain_types):
32+
raise TypeError(f'Next images from imagesBase64 array are not string: {contain_types}')
33+
else:
34+
raise ZeroImagesErrors(f'At least one image base64 expected, got {len(value)}')
35+
return value
36+
37+
def getTaskDict(self) -> Dict[str, Union[str, int, bool]]:
38+
task = {}
39+
task['type'] = self.taskType
40+
task['class'] = self.captchaClass
41+
task['imagesBase64'] = self.imagesBase64
42+
task['metadata'] = self.metadata
43+
if self.userAgent is not None:
44+
task['userAgent'] = self.userAgent
45+
return task

capmonstercloud_client/requests/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from .BinanceTaskProxylessRequest import BinanceTaskProxylessRequest
2929
from .ImpervaCustomTaskRequest import ImpervaCustomTaskRequest
3030
from .ImpervaCustomTaskProxylessRequest import ImpervaCustomTaskProxylessRequest
31+
from .RecognitionComplexImageTaskRequest import RecognitionComplexImageTaskRequest
3132

3233

3334
REQUESTS = ['RecaptchaV2EnterpiseRequest', 'RecaptchaV2EnterpriseProxylessRequest',
@@ -38,4 +39,4 @@
3839
'TenDiCustomTaskRequest', 'TenDiCustomTaskProxylessRequest', 'BasiliskCustomTaskRequest',
3940
'BasiliskCustomTaskProxylessRequest', 'AmazonWafRequest', 'AmazonWafProxylessRequest',
4041
'BinanceTaskRequest', 'BinanceTaskProxylessRequest', 'ImpervaCustomTaskProxylessRequest',
41-
'ImpervaCustomTaskRequest']
42+
'ImpervaCustomTaskRequest', 'RecognitionComplexImageTaskRequest']

capmonstercloud_client/version.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.6.0
1+
2.0.0

examples/imperva.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
import json
88

99
async def solve_captcha_sync(num_requests):
10-
return [await cap_monster_client.solve_captcha(datadome_request) for _ in range(num_requests)]
10+
return [await cap_monster_client.solve_captcha(imperva_request) for _ in range(num_requests)]
1111

1212
async def solve_captcha_async(num_requests):
13-
tasks = [asyncio.create_task(cap_monster_client.solve_captcha(datadome_request))
13+
tasks = [asyncio.create_task(cap_monster_client.solve_captcha(imperva_request))
1414
for _ in range(num_requests)]
1515
return await asyncio.gather(*tasks, return_exceptions=True)
1616

@@ -21,7 +21,7 @@ async def solve_captcha_async(num_requests):
2121
metadata = {"incapsulaScriptBase64": "",
2222
"incapsulaSessionCookie": "SAyLRzdYgUntD6v0r7nFBmxTYGcAAAAArkznhRMmVs/cBynTg3r6YA==",
2323
"reese84UrlEndpoint": "Alarums-Exeunter-Hath-Brese-Banq-Wheth-frangerd-"}
24-
datadome_request = ImpervaCustomTaskProxylessRequest(
24+
imperva_request = ImpervaCustomTaskProxylessRequest(
2525
websiteUrl='https://example.com/login',
2626
metadata=metadata
2727
)

examples/recognitionCIT.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import os
2+
import time
3+
import asyncio
4+
import base64
5+
from capmonstercloudclient.requests import RecognitionComplexImageTaskRequest
6+
from capmonstercloudclient import ClientOptions, CapMonsterClient
7+
8+
async def solve_captcha_sync(num_requests):
9+
return [await cap_monster_client.solve_captcha(oocl_request) for _ in range(num_requests)]
10+
11+
async def solve_captcha_async(num_requests):
12+
tasks = [asyncio.create_task(cap_monster_client.solve_captcha(oocl_request))
13+
for _ in range(num_requests)]
14+
return await asyncio.gather(*tasks, return_exceptions=True)
15+
16+
if __name__ == '__main__':
17+
key = os.getenv('API_KEY')
18+
client_options = ClientOptions(api_key=key)
19+
cap_monster_client = CapMonsterClient(options=client_options)
20+
with open("/path/to/img/0_2.png", 'rb') as f:
21+
bg = base64.b64encode(f.read()).decode("utf-8")
22+
with open("/path/to/img/0_0.png", 'rb') as f:
23+
ring = base64.b64encode(f.read()).decode("utf-8")
24+
with open("/path/to/img/0_1.png", 'rb') as f:
25+
circle = base64.b64encode(f.read()).decode("utf-8")
26+
oocl_request = RecognitionComplexImageTaskRequest(
27+
metadata={"Task": "oocl_rotate_double_new"},
28+
imagesBase64=[bg, ring, circle]
29+
)
30+
nums = 3
31+
32+
# Sync test
33+
sync_start = time.time()
34+
sync_responses = asyncio.run(solve_captcha_sync(nums))
35+
print(f'average execution time sync {1/((time.time()-sync_start)/nums):0.2f} ' \
36+
f'resp/sec\nsolution: {sync_responses[0]}')
37+
38+
# Async test
39+
async_start = time.time()
40+
async_responses = asyncio.run(solve_captcha_async(nums))
41+
print(f'average execution time async {1/((time.time()-async_start)/nums):0.2f} ' \
42+
f'resp/sec\nsolution: {async_responses[0]}')

requirements.txt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,18 @@
1-
aiohttp>=3.7.4
2-
pydantic==2.1.*
1+
aiohappyeyeballs==2.4.4 ; python_version >= "3.9" and python_version <= "3.13"
2+
aiohttp==3.11.10 ; python_version >= "3.9" and python_version <= "3.13"
3+
aiosignal==1.3.1 ; python_version >= "3.9" and python_version <= "3.13"
4+
annotated-types==0.7.0 ; python_version >= "3.9" and python_version <= "3.13"
5+
async-timeout==5.0.1 ; python_version >= "3.9" and python_version <= "3.11"
6+
attrs==24.2.0 ; python_version >= "3.9" and python_version <= "3.13"
7+
frozenlist==1.5.0 ; python_version >= "3.9" and python_version <= "3.13"
8+
idna==3.10 ; python_version >= "3.9" and python_version <= "3.13"
9+
multidict==6.1.0 ; python_version >= "3.9" and python_version <= "3.13"
10+
propcache==0.2.1 ; python_version >= "3.9" and python_version <= "3.13"
11+
pydantic-core==2.27.1 ; python_version >= "3.9" and python_version <= "3.13"
12+
pydantic==2.10.3 ; python_version >= "3.9" and python_version <= "3.13"
13+
typing-extensions==4.12.2 ; python_version >= "3.9" and python_version <= "3.13"
14+
yarl==1.18.3 ; python_version >= "3.9" and python_version <= "3.13"
15+
aiohttp==3.7.4 ; python_version >= "3.6" and python_version <= "3.8"
16+
pydantic==2.10.3 ; python_version >= "3.8" and python_version < "3.9"
17+
pydantic==2.5.3 ; python_version >= "3.7" and python_version < "3.8"
18+
pydantic==2.1.* ; python_version < "3.7"

test/cit_test.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import unittest
2+
import urllib
3+
import base64
4+
from pydantic.error_wrappers import ValidationError
5+
from capmonstercloudclient.requests import RecognitionComplexImageTaskRequest
6+
from capmonstercloudclient.exceptions import NumbersImagesErrors, TaskNotDefinedError, ZeroImagesErrors, \
7+
UserAgentNotDefinedError
8+
9+
def read_image(image_url: str,):
10+
image_bytes = urllib.request.urlopen(image_url).read()
11+
return base64.b64encode(image_bytes).decode('utf-8')
12+
13+
class RecognitionCITImageRequestTest(unittest.TestCase):
14+
15+
websiteUrlExample = 'https://lessons.zennolab.com/captchas/recaptcha/v2_simple.php?level=middle'
16+
imageUrlsExamples = ['https://i.postimg.cc/H8yBD5FJ/0-2.png', 'https://i.postimg.cc/rz0hrXz8/0-0.png',
17+
'https://i.postimg.cc/qgP1cbC2/0-1.png']
18+
imageBase64Examples = [read_image(i) for i in imageUrlsExamples]
19+
metadataExample = {"Task": "oocl_rotate_double_new"}
20+
21+
def testImagesTypes(self):
22+
23+
with self.assertRaises(ValidationError):
24+
request = RecognitionComplexImageTaskRequest(metadata=RecognitionCITImageRequestTest.metadataExample,
25+
imagesBase64='[]')
26+
27+
28+
def testImagesFilling(self):
29+
30+
with self.assertRaises(ZeroImagesErrors,
31+
msg='Empty array imagesBase64 must be cause ZeroImagesErrors'):
32+
request = RecognitionComplexImageTaskRequest(metadata=RecognitionCITImageRequestTest.metadataExample,
33+
imagesBase64=[])
34+
35+
def testAllRequiredFieldsFilling(self):
36+
required_fields = ['class', 'type', 'metadata']
37+
metadata_fields = ['Task']
38+
request = RecognitionComplexImageTaskRequest(
39+
metadata=RecognitionCITImageRequestTest.metadataExample,
40+
imagesBase64=RecognitionCITImageRequestTest.imageBase64Examples)
41+
request_dict = request.getTaskDict()
42+
for i in required_fields:
43+
self.assertTrue(i in list(request_dict.keys()),
44+
msg=f'Required field {i} not in {request_dict}')
45+
46+
metadata_dict = request_dict['metadata']
47+
for i in metadata_fields:
48+
self.assertTrue(i in list(metadata_dict.keys()),
49+
msg=f'Required field {i} not in {request_dict}')
50+
51+
self.assertEqual(request_dict['class'], 'recognition')
52+
self.assertEqual(request_dict['type'], 'ComplexImageTask')
53+
54+
def testTaskDefined(self):
55+
with self.assertRaises(TaskNotDefinedError,
56+
msg='Expect that empty "Task" field will be cause TaskNotDefinedError'):
57+
request = RecognitionComplexImageTaskRequest(metadata={},
58+
imagesBase64=RecognitionCITImageRequestTest.imageBase64Examples)
59+
60+
with self.assertRaises(TaskNotDefinedError):
61+
request = RecognitionComplexImageTaskRequest(metadata={'dsfsdf': 'sdfsdf'},
62+
imagesUrls=RecognitionCITImageRequestTest.imageUrlsExamples)
63+
64+
def testExtraMetadata(self):
65+
with self.assertRaises(TypeError,
66+
msg='Expect that extra metadata fields will be cause TypeError'):
67+
request = RecognitionComplexImageTaskRequest(metadata={"Task": "oocl_rotate_new", 'asd': 'asd'},
68+
imagesBase64=RecognitionCITImageRequestTest.imageBase64Examples)
69+
70+
if __name__ == '__main__':
71+
unittest.main()

0 commit comments

Comments
 (0)