From 51b1a420ac335410958317eee31f2f89b158ae68 Mon Sep 17 00:00:00 2001 From: AndreiDrang Date: Wed, 7 Oct 2026 21:23:09 +0300 Subject: [PATCH 1/6] bump python versions --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 13fdd95..ec7f917 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,6 +78,8 @@ classifiers = [ "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", "Framework :: AsyncIO", "Operating System :: Unix", "Operating System :: Microsoft :: Windows", From 92d7b169d172f81cd6038e5ba8e77b4d5a57825d Mon Sep 17 00:00:00 2001 From: AndreiDrang Date: Wed, 7 Oct 2026 21:23:45 +0300 Subject: [PATCH 2/6] bump versions --- .github/workflows/build.yml | 2 +- .github/workflows/install.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9cff0e0..35c7850 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v7 diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml index 8d89bb0..ff4ff1b 100644 --- a/.github/workflows/install.yml +++ b/.github/workflows/install.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v7 From bb594d97fe641f79578957d2cb064ff581057a8f Mon Sep 17 00:00:00 2001 From: AndreiDrang Date: Wed, 7 Oct 2026 21:41:30 +0300 Subject: [PATCH 3/6] test(core): add mocked unit tests for sync and async instruments Why: * instrument polling and error branches were only exercised against the live capsolver api, so __get_result and the error paths stayed uncovered whenever the live suite cannot run What: * cover the full processing/polling lifecycle of SIOCaptchaInstrument and AIOCaptchaInstrument with scripted responses at the http boundary: ready-at-creation without polling, polling until ready/failed with taskId propagation, rejected creation marked failed, invalid status codes raising, the 15-attempt silent-server unsolvable default, and send_post_request success/error * cover failed url download returning empty base64 in FileInstrument * register the new test files in the tests/AGENTS.md layout Changes: * tests/test_sio_captcha_instrument.py: new mocked suite (11 test items) * tests/test_aio_captcha_instrument.py: new mocked suite (11 test items) * tests/test_instrument.py: add aio download-failure test, use plain membership checks * tests/AGENTS.md: list the two new test files Stats: * 4 files changed * +389/-2 lines Co-authored-by: Claude --- tests/AGENTS.md | 2 + tests/test_aio_captcha_instrument.py | 186 +++++++++++++++++++++++++++ tests/test_instrument.py | 18 ++- tests/test_sio_captcha_instrument.py | 185 ++++++++++++++++++++++++++ 4 files changed, 389 insertions(+), 2 deletions(-) create mode 100644 tests/test_aio_captcha_instrument.py create mode 100644 tests/test_sio_captcha_instrument.py diff --git a/tests/AGENTS.md b/tests/AGENTS.md index 09f9c8f..806195c 100644 --- a/tests/AGENTS.md +++ b/tests/AGENTS.md @@ -11,6 +11,8 @@ tests/ ├── conftest.py # BaseTest class, rate-limiting fixtures ├── test_core.py # CaptchaParams, enums, context managers ├── test_instrument.py # FileInstrument, CaptchaInstrumentBase +├── test_sio_captcha_instrument.py # SIOCaptchaInstrument: mocked sync task processing/polling +├── test_aio_captcha_instrument.py # AIOCaptchaInstrument: mocked async task processing/polling ├── test_control.py # Control: get_balance, create_task, get_task_result ├── test_recaptcha.py # ReCaptcha V2/V3/Enterprise ├── test_cloudflare.py # Cloudflare Turnstile/Challenge diff --git a/tests/test_aio_captcha_instrument.py b/tests/test_aio_captcha_instrument.py new file mode 100644 index 0000000..5914496 --- /dev/null +++ b/tests/test_aio_captcha_instrument.py @@ -0,0 +1,186 @@ +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from tests.conftest import BaseTest +from python3_capsolver.core.base import CaptchaParams +from python3_capsolver.core.enum import CaptchaTypeEnm, EndpointPostfixEnm +from python3_capsolver.core.const import REQUEST_URL +from python3_capsolver.core.aio_captcha_instrument import AIOCaptchaInstrument + +API_KEY_STUB = "unit-test-api-key-000000000000000000000000" +CREATE_TASK_URL = "https://unit.test/createTask" +GET_TASK_RESULT_URL = "https://unit.test/getTaskResult" + + +def create_captcha_params() -> CaptchaParams: + """ + Build params pointing to a fake API host with zero sleep between polls: + no real request can leave the mocked `aiohttp.ClientSession.post` boundary + """ + return CaptchaParams( + api_key=API_KEY_STUB, + captcha_type=CaptchaTypeEnm.Control, + sleep_time=0, + request_url="https://unit.test/", + ) + + +def aio_response(payload=None, status: int = 200, reason: str = "unit test failure") -> MagicMock: + """ + Build fake response for the `aiohttp` boundary, usable as async context manager. + + For non-parsable bodies (``payload is None``) any ``json()`` call raises - + an error status must never be parsed as a success body + """ + response = MagicMock() + response.status = status + response.reason = reason + if payload is None: + response.json = AsyncMock(side_effect=AssertionError("body of an error response must not be parsed")) + else: + response.json = AsyncMock(return_value=payload) + response.__aenter__.return_value = response + return response + + +class TestAIOCaptchaInstrument(BaseTest): + """ + Mocked unit tests for AIOCaptchaInstrument + """ + + @patch("python3_capsolver.core.aio_captcha_instrument.aiohttp.ClientSession.post") + async def test_returns_created_result_without_polling_when_task_is_ready(self, mock_post): + mock_post.return_value = aio_response( + {"errorId": 0, "taskId": "task-ready-1", "status": "ready", "solution": {"text": "wg6x"}} + ) + + result = await AIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha() + + assert result["errorId"] == 0 + assert result["status"] == "ready" + assert result["taskId"] == "task-ready-1" + assert result["solution"] == {"text": "wg6x"} + # task was solved at creation - no `getTaskResult` polling must follow + mock_post.assert_called_once() + assert mock_post.call_args.args[0] == CREATE_TASK_URL + assert mock_post.call_args.kwargs["json"]["task"] == {"type": "Control"} + assert mock_post.call_args.kwargs["json"]["clientKey"] == API_KEY_STUB + + @patch("python3_capsolver.core.aio_captcha_instrument.aiohttp.ClientSession.post") + async def test_polls_get_task_result_until_ready(self, mock_post): + mock_post.side_effect = [ + aio_response({"errorId": 0, "taskId": "task-42", "status": "processing"}), + aio_response({"errorId": 0, "taskId": "task-42", "status": "processing"}), + aio_response({"errorId": 0, "taskId": "task-42", "status": "ready", "solution": {"text": "ab12"}}), + ] + + result = await AIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha() + + assert result["errorId"] == 0 + assert result["status"] == "ready" + assert result["solution"] == {"text": "ab12"} + assert [call.args[0] for call in mock_post.call_args_list] == [ + CREATE_TASK_URL, + GET_TASK_RESULT_URL, + GET_TASK_RESULT_URL, + ] + # created taskId must be propagated to every polling request + assert mock_post.call_args_list[1].kwargs["json"]["taskId"] == "task-42" + assert mock_post.call_args_list[2].kwargs["json"]["taskId"] == "task-42" + + @patch("python3_capsolver.core.aio_captcha_instrument.aiohttp.ClientSession.post") + async def test_returns_failed_result_when_polling_reports_failure(self, mock_post): + mock_post.side_effect = [ + aio_response({"errorId": 0, "taskId": "task-42", "status": "processing"}), + aio_response({"errorId": 1, "errorCode": "ERROR_TASK_NOT_FOUND", "status": "failed"}), + ] + + result = await AIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha() + + assert result["errorId"] == 1 + assert result["status"] == "failed" + assert result["errorCode"] == "ERROR_TASK_NOT_FOUND" + assert len(mock_post.call_args_list) == 2 + + @patch("python3_capsolver.core.aio_captcha_instrument.aiohttp.ClientSession.post") + async def test_marks_result_failed_when_task_creation_is_rejected(self, mock_post): + mock_post.return_value = aio_response( + {"errorId": 1, "errorCode": "ERROR_KEY_DOES_NOT_EXIST", "errorDescription": "Client key is invalid"} + ) + + result = await AIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha() + + assert result["errorId"] == 1 + assert result["status"] == "failed" + assert result["errorCode"] == "ERROR_KEY_DOES_NOT_EXIST" + assert result["errorDescription"] == "Client key is invalid" + # rejected creation must not trigger result polling + mock_post.assert_called_once() + + @patch("python3_capsolver.core.aio_captcha_instrument.aiohttp.ClientSession.post") + async def test_parses_error_payload_when_status_code_is_valid_error_status(self, mock_post): + mock_post.return_value = aio_response({"errorId": 1, "errorCode": "ERROR_WRONG_USERKEY"}, status=400) + + result = await AIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha() + + assert result["status"] == "failed" + assert result["errorCode"] == "ERROR_WRONG_USERKEY" + + @patch("python3_capsolver.core.aio_captcha_instrument.aiohttp.ClientSession.post") + async def test_returns_unsolvable_default_when_server_never_finishes(self, mock_post): + mock_post.side_effect = [aio_response({"errorId": 0, "taskId": "task-silent", "status": "processing"})] * 16 + + result = await AIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha() + + assert result["errorId"] == 1 + assert result["errorCode"] == "ERROR_CAPTCHA_UNSOLVABLE" + assert result["errorDescription"] == "Captcha not recognized" + assert result["status"] == "failed" + assert result["taskId"] == "task-silent" + # 1 `createTask` + 15 polling attempts, then the instrument gives up + assert len(mock_post.call_args_list) == 16 + + @pytest.mark.parametrize("bad_status", [404, 500]) + @patch("python3_capsolver.core.aio_captcha_instrument.aiohttp.ClientSession.post") + async def test_raises_when_create_task_returns_invalid_status(self, mock_post, bad_status): + mock_post.return_value = aio_response(status=bad_status, reason="Internal Server Error") + + with pytest.raises(ValueError): + await AIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha() + + mock_post.assert_called_once() + + @patch("python3_capsolver.core.aio_captcha_instrument.aiohttp.ClientSession.post") + async def test_raises_when_get_task_result_returns_invalid_status(self, mock_post): + mock_post.side_effect = [ + aio_response({"errorId": 0, "taskId": "task-42", "status": "processing"}), + aio_response(status=503, reason="Service Unavailable"), + ] + + with pytest.raises(ValueError): + await AIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha() + + assert len(mock_post.call_args_list) == 2 + + @patch("python3_capsolver.core.aio_captcha_instrument.aiohttp.ClientSession.post") + async def test_send_post_request_returns_decoded_payload(self, mock_post): + mock_post.return_value = aio_response({"errorId": 0, "balance": 107.7}) + + result = await AIOCaptchaInstrument.send_post_request( + payload={"clientKey": API_KEY_STUB}, + url_postfix=EndpointPostfixEnm.GET_BALANCE, + ) + + assert result == {"errorId": 0, "balance": 107.7} + assert mock_post.call_args.args[0] == f"{REQUEST_URL}/{EndpointPostfixEnm.GET_BALANCE.value}" + assert mock_post.call_args.kwargs["json"] == {"clientKey": API_KEY_STUB} + + @patch("python3_capsolver.core.aio_captcha_instrument.aiohttp.ClientSession.post") + async def test_send_post_request_raises_for_error_status(self, mock_post): + mock_post.return_value = aio_response(status=403) + + with pytest.raises(ValueError): + await AIOCaptchaInstrument.send_post_request(payload={"clientKey": API_KEY_STUB}) + + mock_post.assert_called_once() diff --git a/tests/test_instrument.py b/tests/test_instrument.py index 9b6c621..577ba98 100644 --- a/tests/test_instrument.py +++ b/tests/test_instrument.py @@ -1,3 +1,5 @@ +from unittest.mock import MagicMock, patch + import pytest from tests.conftest import BaseTest @@ -11,8 +13,8 @@ class TestFileInstrument(BaseTest): """ def test_file_processing_exist(self): - assert "file_processing" in FileInstrument.__dict__.keys() - assert "aio_file_processing" in FileInstrument.__dict__.keys() + assert "file_processing" in FileInstrument.__dict__ + assert "aio_file_processing" in FileInstrument.__dict__ def test_captcha_base64(self): assert self.read_image_as_str() == FileInstrument().file_processing(captcha_base64=self.read_image()) @@ -55,3 +57,15 @@ def test_file_processing_err(self): async def test_aio_file_processing_err(self): with pytest.raises(ValueError): await FileInstrument().aio_file_processing() + + @patch("python3_capsolver.core.captcha_instrument.aiohttp.ClientSession.get") + async def test_aio_captcha_link_download_err(self, mock_get): + mock_response = MagicMock() + mock_response.status = 404 + mock_response.__aenter__.return_value = mock_response + mock_get.return_value = mock_response + + result = await FileInstrument().aio_file_processing(captcha_link=self.image_captcha_url_example) + + # unreachable link returns empty content instead of raising + assert result == "" diff --git a/tests/test_sio_captcha_instrument.py b/tests/test_sio_captcha_instrument.py new file mode 100644 index 0000000..6eb9fdc --- /dev/null +++ b/tests/test_sio_captcha_instrument.py @@ -0,0 +1,185 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from tests.conftest import BaseTest +from python3_capsolver.core.base import CaptchaParams +from python3_capsolver.core.enum import CaptchaTypeEnm, EndpointPostfixEnm +from python3_capsolver.core.const import REQUEST_URL +from python3_capsolver.core.sio_captcha_instrument import SIOCaptchaInstrument + +API_KEY_STUB = "unit-test-api-key-000000000000000000000000" +CREATE_TASK_URL = "https://unit.test/createTask" +GET_TASK_RESULT_URL = "https://unit.test/getTaskResult" + + +def create_captcha_params() -> CaptchaParams: + """ + Build params pointing to a fake API host with zero sleep between polls: + no real request can leave the mocked `Session.post` boundary + """ + return CaptchaParams( + api_key=API_KEY_STUB, + captcha_type=CaptchaTypeEnm.Control, + sleep_time=0, + request_url="https://unit.test/", + ) + + +def sio_response(payload=None, status_code: int = 200) -> MagicMock: + """ + Build fake response for the `requests` boundary. + + For non-parsable bodies (``payload is None``) any ``json()`` call raises - + an error status must never be parsed as a success body + """ + response = MagicMock() + response.status_code = status_code + if payload is None: + response.json.side_effect = AssertionError("body of an error response must not be parsed") + else: + response.json.return_value = payload + response.raise_for_status.return_value = None + return response + + +class TestSIOCaptchaInstrument(BaseTest): + """ + Mocked unit tests for SIOCaptchaInstrument + """ + + @patch("python3_capsolver.core.sio_captcha_instrument.requests.Session.post") + def test_returns_created_result_without_polling_when_task_is_ready(self, mock_post): + mock_post.return_value = sio_response( + {"errorId": 0, "taskId": "task-ready-1", "status": "ready", "solution": {"text": "wg6x"}} + ) + + result = SIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha() + + assert result["errorId"] == 0 + assert result["status"] == "ready" + assert result["taskId"] == "task-ready-1" + assert result["solution"] == {"text": "wg6x"} + # task was solved at creation - no `getTaskResult` polling must follow + mock_post.assert_called_once() + assert mock_post.call_args.args[0] == CREATE_TASK_URL + assert mock_post.call_args.kwargs["json"]["task"] == {"type": "Control"} + assert mock_post.call_args.kwargs["json"]["clientKey"] == API_KEY_STUB + + @patch("python3_capsolver.core.sio_captcha_instrument.requests.Session.post") + def test_polls_get_task_result_until_ready(self, mock_post): + mock_post.side_effect = [ + sio_response({"errorId": 0, "taskId": "task-42", "status": "processing"}), + sio_response({"errorId": 0, "taskId": "task-42", "status": "processing"}), + sio_response({"errorId": 0, "taskId": "task-42", "status": "ready", "solution": {"text": "ab12"}}), + ] + + result = SIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha() + + assert result["errorId"] == 0 + assert result["status"] == "ready" + assert result["solution"] == {"text": "ab12"} + assert [call.args[0] for call in mock_post.call_args_list] == [ + CREATE_TASK_URL, + GET_TASK_RESULT_URL, + GET_TASK_RESULT_URL, + ] + # created taskId must be propagated to every polling request + assert mock_post.call_args_list[1].kwargs["json"]["taskId"] == "task-42" + assert mock_post.call_args_list[2].kwargs["json"]["taskId"] == "task-42" + + @patch("python3_capsolver.core.sio_captcha_instrument.requests.Session.post") + def test_returns_failed_result_when_polling_reports_failure(self, mock_post): + mock_post.side_effect = [ + sio_response({"errorId": 0, "taskId": "task-42", "status": "processing"}), + sio_response({"errorId": 1, "errorCode": "ERROR_TASK_NOT_FOUND", "status": "failed"}), + ] + + result = SIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha() + + assert result["errorId"] == 1 + assert result["status"] == "failed" + assert result["errorCode"] == "ERROR_TASK_NOT_FOUND" + assert len(mock_post.call_args_list) == 2 + + @patch("python3_capsolver.core.sio_captcha_instrument.requests.Session.post") + def test_marks_result_failed_when_task_creation_is_rejected(self, mock_post): + mock_post.return_value = sio_response( + {"errorId": 1, "errorCode": "ERROR_KEY_DOES_NOT_EXIST", "errorDescription": "Client key is invalid"} + ) + + result = SIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha() + + assert result["errorId"] == 1 + assert result["status"] == "failed" + assert result["errorCode"] == "ERROR_KEY_DOES_NOT_EXIST" + assert result["errorDescription"] == "Client key is invalid" + # rejected creation must not trigger result polling + mock_post.assert_called_once() + + @patch("python3_capsolver.core.sio_captcha_instrument.requests.Session.post") + def test_parses_error_payload_when_status_code_is_valid_error_status(self, mock_post): + mock_post.return_value = sio_response({"errorId": 1, "errorCode": "ERROR_WRONG_USERKEY"}, status_code=400) + + result = SIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha() + + assert result["status"] == "failed" + assert result["errorCode"] == "ERROR_WRONG_USERKEY" + + @patch("python3_capsolver.core.sio_captcha_instrument.requests.Session.post") + def test_returns_unsolvable_default_when_server_never_finishes(self, mock_post): + mock_post.side_effect = [sio_response({"errorId": 0, "taskId": "task-silent", "status": "processing"})] * 16 + + result = SIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha() + + assert result["errorId"] == 1 + assert result["errorCode"] == "ERROR_CAPTCHA_UNSOLVABLE" + assert result["errorDescription"] == "Captcha not recognized" + assert result["status"] == "failed" + assert result["taskId"] == "task-silent" + # 1 `createTask` + 15 polling attempts, then the instrument gives up + assert len(mock_post.call_args_list) == 16 + + @pytest.mark.parametrize("bad_status", [404, 500]) + @patch("python3_capsolver.core.sio_captcha_instrument.requests.Session.post") + def test_raises_when_create_task_returns_invalid_status(self, mock_post, bad_status): + mock_post.return_value = sio_response(status_code=bad_status) + + with pytest.raises(ValueError): + SIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha() + + mock_post.assert_called_once() + + @patch("python3_capsolver.core.sio_captcha_instrument.requests.Session.post") + def test_raises_when_get_task_result_returns_invalid_status(self, mock_post): + mock_post.side_effect = [ + sio_response({"errorId": 0, "taskId": "task-42", "status": "processing"}), + sio_response(status_code=503), + ] + + with pytest.raises(ValueError): + SIOCaptchaInstrument(captcha_params=create_captcha_params()).processing_captcha() + + assert len(mock_post.call_args_list) == 2 + + @patch("python3_capsolver.core.sio_captcha_instrument.requests.Session.post") + def test_send_post_request_returns_decoded_payload(self, mock_post): + mock_post.return_value = sio_response({"errorId": 0, "balance": 107.7}) + + result = SIOCaptchaInstrument.send_post_request( + payload={"clientKey": API_KEY_STUB}, + url_postfix=EndpointPostfixEnm.GET_BALANCE, + ) + + assert result == {"errorId": 0, "balance": 107.7} + assert mock_post.call_args.args[0] == f"{REQUEST_URL}/{EndpointPostfixEnm.GET_BALANCE.value}" + assert mock_post.call_args.kwargs["json"] == {"clientKey": API_KEY_STUB} + + @patch("python3_capsolver.core.sio_captcha_instrument.requests.Session.post") + def test_send_post_request_raises_for_error_status(self, mock_post): + mock_post.return_value = sio_response(status_code=403) + + with pytest.raises(ValueError): + SIOCaptchaInstrument.send_post_request(payload={"clientKey": API_KEY_STUB}) + + mock_post.assert_called_once() From 2f14b384fe74e99c71b671f73e4591f25bd0655b Mon Sep 17 00:00:00 2001 From: AndreiDrang Date: Wed, 7 Oct 2026 21:41:38 +0300 Subject: [PATCH 4/6] refactor(core): fix forward refs and modernize instrument annotations Why: * the quoted "CaptchaParams" forward references in both instruments resolved to no import, so static analysis flagged them as undefined * Optional[...] and quoted annotations predate lazy annotation support What: * add a TYPE_CHECKING-guarded CaptchaParams import to break the annotation-only import cycle with base.py * add from __future__ import annotations and switch the single Optional[dict] parameter to dict | None (lazy, so python 3.8+ stays supported) * unquote the CaptchaParams parameter annotation no runtime behavior change: the guard never executes and annotations are never evaluated; the full mocked suite passes identically Changes: * src/python3_capsolver/core/sio_captcha_instrument.py: typing hygiene * src/python3_capsolver/core/aio_captcha_instrument.py: typing hygiene Stats: * 2 files changed * +10/-4 lines Co-authored-by: Claude --- src/python3_capsolver/core/aio_captcha_instrument.py | 11 ++++++++--- src/python3_capsolver/core/sio_captcha_instrument.py | 11 ++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/python3_capsolver/core/aio_captcha_instrument.py b/src/python3_capsolver/core/aio_captcha_instrument.py index 814b532..452edf8 100644 --- a/src/python3_capsolver/core/aio_captcha_instrument.py +++ b/src/python3_capsolver/core/aio_captcha_instrument.py @@ -1,6 +1,8 @@ +from __future__ import annotations + import asyncio import logging -from typing import Optional +from typing import TYPE_CHECKING from urllib import parse import aiohttp @@ -11,6 +13,9 @@ from .serializer import CaptchaResponseSer from .captcha_instrument import CaptchaInstrumentBase +if TYPE_CHECKING: + from .base import CaptchaParams + __all__ = ("AIOCaptchaInstrument",) @@ -19,7 +24,7 @@ class AIOCaptchaInstrument(CaptchaInstrumentBase): Instrument for working with async captcha """ - def __init__(self, captcha_params: "CaptchaParams"): + def __init__(self, captcha_params: CaptchaParams): super().__init__() self.captcha_params = captcha_params self.created_task_data = CaptchaResponseSer @@ -99,7 +104,7 @@ async def __get_result(self, url_postfix: str = EndpointPostfixEnm.GET_TASK_RESU @staticmethod async def send_post_request( - payload: Optional[dict] = None, + payload: dict | None = None, url_postfix: EndpointPostfixEnm = EndpointPostfixEnm.GET_BALANCE, ) -> dict: """ diff --git a/src/python3_capsolver/core/sio_captcha_instrument.py b/src/python3_capsolver/core/sio_captcha_instrument.py index d9c5160..9e2c7d4 100644 --- a/src/python3_capsolver/core/sio_captcha_instrument.py +++ b/src/python3_capsolver/core/sio_captcha_instrument.py @@ -1,6 +1,8 @@ +from __future__ import annotations + import time import logging -from typing import Optional +from typing import TYPE_CHECKING from urllib import parse import requests @@ -12,6 +14,9 @@ from .serializer import CaptchaResponseSer from .captcha_instrument import CaptchaInstrumentBase +if TYPE_CHECKING: + from .base import CaptchaParams + __all__ = ("SIOCaptchaInstrument",) @@ -20,7 +25,7 @@ class SIOCaptchaInstrument(CaptchaInstrumentBase): Instrument for working with sync captcha """ - def __init__(self, captcha_params: "CaptchaParams"): + def __init__(self, captcha_params: CaptchaParams): super().__init__() self.captcha_params = captcha_params self.created_task_data = CaptchaResponseSer @@ -106,7 +111,7 @@ def __get_result(self, url_postfix: str = EndpointPostfixEnm.GET_TASK_RESULT.val @staticmethod def send_post_request( - payload: Optional[dict] = None, + payload: dict | None = None, session: requests.Session = requests.Session(), url_postfix: EndpointPostfixEnm = EndpointPostfixEnm.GET_BALANCE, ) -> dict: From 1285937ceefdb9a3ee3f58823dbba13e278bf6d5 Mon Sep 17 00:00:00 2001 From: AndreiDrang Date: Wed, 7 Oct 2026 21:43:07 +0300 Subject: [PATCH 5/6] bump to 1.2.2 --- src/python3_capsolver/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/python3_capsolver/__version__.py b/src/python3_capsolver/__version__.py index a955fda..bc86c94 100644 --- a/src/python3_capsolver/__version__.py +++ b/src/python3_capsolver/__version__.py @@ -1 +1 @@ -__version__ = "1.2.1" +__version__ = "1.2.2" From 858cbd4319ce5d786d822296672f2b0289d501f4 Mon Sep 17 00:00:00 2001 From: AndreiDrang Date: Wed, 7 Oct 2026 21:46:06 +0300 Subject: [PATCH 6/6] Update pyproject.toml --- pyproject.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ec7f917..e208d54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,10 +65,9 @@ keywords = [ "captcha", "imperva", "binance" ] -license = {text = "MIT License"} +license = "MIT" +license-files = ["LICENSE"] classifiers = [ - "License :: OSI Approved :: MIT License", - "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", "Development Status :: 5 - Production/Stable", "Programming Language :: Python", "Programming Language :: Python :: 3",