diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 88caabd0..6a42cd31 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -23,7 +23,7 @@ jobs:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml
index 15d7d821..2886378a 100644
--- a/.github/workflows/install.yml
+++ b/.github/workflows/install.yml
@@ -23,7 +23,7 @@ jobs:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 6b5199ad..a862597a 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -27,7 +27,7 @@ jobs:
python-version: ["3.12"]
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml
index 03089cfe..8896470d 100644
--- a/.github/workflows/sphinx.yml
+++ b/.github/workflows/sphinx.yml
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- uses: actions/setup-python@v6
- name: Build docs requirements
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 993d26e2..9dbc905e 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -33,7 +33,7 @@ jobs:
python-version: ["3.11"]
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
@@ -48,7 +48,7 @@ jobs:
run: make tests
- name: Upload coverage to Codecov
- uses: codecov/codecov-action@v5
+ uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ${{github.workspace}}/coverage/coverage.xml
diff --git a/.github/workflows/zai-code-bot.yml b/.github/workflows/zai-code-bot.yml
new file mode 100644
index 00000000..6d6abb10
--- /dev/null
+++ b/.github/workflows/zai-code-bot.yml
@@ -0,0 +1,45 @@
+name: Z.ai Code Bot
+
+concurrency:
+ group: zai-bot-${{ github.event.pull_request.number || github.event.issue.number }}
+ cancel-in-progress: false
+
+on:
+ pull_request:
+ types:
+ - opened
+ - synchronize
+ - reopened
+ - ready_for_review
+ branches:
+ - main
+ - prod
+ issue_comment:
+ types:
+ - created
+ pull_request_review_comment:
+ types:
+ - created
+
+permissions:
+ contents: read
+ pull-requests: write
+ issues: write
+
+jobs:
+ zai-bot:
+ if: |
+ (github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
+ (github.event_name == 'issue_comment' && github.event.issue.pull_request) ||
+ (github.event_name == 'pull_request_review_comment' && github.event.pull_request.draft == false)
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+
+ - name: Run Z.ai Bot
+ uses: AndreiDrang/zai-code-bot@main
+ with:
+ ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
+ ZAI_MODEL: ${{ vars.ZAI_MODEL || 'glm-5.1' }}
+ GITHUB_TOKEN: ${{ github.token }}
diff --git a/.gitignore b/.gitignore
index bad8eb9a..cec94e62 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,4 @@ env/
/src/coverage/lcov.info
/docs/_build/
/coverage/
+.osgrep
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000..9f1b394d
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,68 @@
+# PROJECT KNOWLEDGE BASE
+
+**Generated:** 2026-02-20
+**Commit:** 86ee3e6
+**Branch:** master
+
+## OVERVIEW
+Python 3.9+ library for RuCaptcha/2Captcha/DeathByCaptcha service APIs. Supports 30+ CAPTCHA types with dual sync/async interfaces.
+
+## STRUCTURE
+```
+./
+├── src/python_rucaptcha/ # Main package (30+ captcha modules)
+│ └── core/ # Base classes, serializers, enums
+├── tests/ # Pytest test suite (23+ files)
+├── docs/ # Sphinx documentation
+├── pyproject.toml # Build config (black 110, isort, pytest)
+└── Makefile # Build automation
+```
+
+## WHERE TO LOOK
+| Task | Location | Notes |
+|------|----------|-------|
+| Add new CAPTCHA | `src/python_rucaptcha/` | Create module inheriting `BaseCaptcha` |
+| Modify API flow | `src/python_rucaptcha/core/base.py` | `_processing_response()` methods |
+| Change serialization | `src/python_rucaptcha/core/serializer.py` | `MyBaseModel` class |
+| Add CAPTCHA enum | `src/python_rucaptcha/core/enums.py` | 25+ enum classes |
+| Run tests | `tests/` | Requires `RUCAPTCHA_KEY` env var |
+
+## CODE MAP
+| Symbol | Type | Location | Role |
+|--------|------|----------|------|
+| BaseCaptcha | Class | core/base.py | Parent for all captcha solvers |
+| MyBaseModel | Class | core/serializer.py | msgspec Struct wrapper |
+| CaptchaOptionsSer | Class | core/config.py | Service URL abstraction |
+| MyEnum | Class | core/enums.py | Custom enum with utils |
+
+## CONVENTIONS
+- **Line length**: 110 chars (pyproject.toml)
+- **Async mode**: `asyncio_mode = auto` in pytest
+- **No tox**: Uses Makefile directly for test/lint
+- **Import order**: isort with black profile
+
+## ANTI-PATTERNS (THIS PROJECT)
+- No TODO/FIXME/DEPRECATED comments in code
+- No explicit "DO NOT" directives
+- Logging warnings output full result objects (potential sensitive data)
+
+## UNIQUE STYLES
+- **25+ custom enums**: Each CAPTCHA type has dedicated enum (e.g., `HCaptchaEnm`)
+- **Service abstraction**: Unified API for 2Captcha/RuCaptcha/DeathByCaptcha
+- **msgspec**: Fast serialization (replaced pydantic v6.0)
+- **Dual sync/async**: Every captcha class has both handlers
+
+## COMMANDS
+```bash
+make install # pip install -e .
+make tests # Run pytest with coverage
+make lint # autoflake + black + isort check
+make build # Build package
+make doc # Build Sphinx docs
+```
+
+## NOTES
+- Tests require live API keys (`RUCAPTCHA_KEY`, `DEATHBYCAPTCHA_KEY`)
+- CI runs on Python 3.11 (tests) / 3.12 (lint) — version mismatch
+- No cross-platform testing (only ubuntu-latest)
+- x.py in root is debug script (non-standard)
diff --git a/README.md b/README.md
index f898305c..6edea8e2 100644
--- a/README.md
+++ b/README.md
@@ -1,70 +1,160 @@
# python-rucaptcha
-[](https://vyjava.xyz/dashboard/image/45247a56-3332-48ee-8df8-fc95bcfc52f0)
-
-
-
[](https://badge.fury.io/py/python-rucaptcha)
[](https://badge.fury.io/py/python-rucaptcha)
[](https://pepy.tech/project/python-rucaptcha)
-[](https://andreidrang.github.io/python-rucaptcha/)
+[](https://andreidrang.github.io/python-rucaptcha/)
-[](https://codeclimate.com/github/AndreiDrang/python-rucaptcha/maintainability)
-[](https://www.codacy.com/gh/AndreiDrang/python-rucaptcha/dashboard?utm_source=github.com&utm_medium=referral&utm_content=AndreiDrang/python-rucaptcha&utm_campaign=Badge_Grade)
-[](https://codecov.io/gh/AndreiDrang/python-rucaptcha)
+**Python 3.9+ library to solve CAPTCHAs automatically using RuCaptcha, 2Captcha, or DeathByCaptcha services.**
-[](https://github.com/AndreiDrang/python-rucaptcha/actions/workflows/sphinx.yml)
-[](https://github.com/AndreiDrang/python-rucaptcha/actions/workflows/build.yml)
-[](https://github.com/AndreiDrang/python-rucaptcha/actions/workflows/install.yml)
-[](https://github.com/AndreiDrang/python-rucaptcha/actions/workflows/test.yml)
-[](https://github.com/AndreiDrang/python-rucaptcha/actions/workflows/lint.yml)
+## What is this?
-Python3 library for [RuCaptcha](https://rucaptcha.com/?from=4170435) and [2Captcha](https://2captcha.com/?from=4170435) service API.
+This library automates CAPTCHA solving by connecting to third-party services. When your code encounters a CAPTCHA, python-rucaptcha sends it to the service, waits for a human to solve it, and returns the solution to your application.
-Tested on UNIX based OS.
+**Supports 30+ CAPTCHA types:**
+- reCAPTCHA v2/v3, hCaptcha, Cloudflare Turnstile
+- Image captchas, Audio captchas
+- GeeTest, KeyCaptcha, Amazon WAF, Tencent
+- And many more...
-The library is intended for software developers and is used to work with the [RuCaptcha](https://rucaptcha.com/?from=4170435) and [2Captcha](https://2captcha.com/?from=4170435) service API.
+## Quick Start
-Support of the service [Death By Captcha](https://deathbycaptcha.com?refid=1237267242) is integrated into this library, more information in the library documentation or in the [service docs](https://deathbycaptcha.com/api/2captcha?refid=1237267242).
+### 1. Install
+
+```bash
+pip install python-rucaptcha
+```
-Application in [RuCaptcha software](https://rucaptcha.com/software/python-rucaptcha) and [2Captcha software](https://2captcha.com/software/python-rucaptcha).
+### 2. Get an API Key
-## How to install?
+Sign up at [RuCaptcha](https://rucaptcha.com) or [2Captcha](https://2captcha.com), then copy your API key from the dashboard.
-### pip
+### 3. Solve a CAPTCHA
-```bash
-pip install python-rucaptcha
+```python
+from python_rucaptcha import HCaptcha
+
+# Your API key
+key = "your_api_key_here"
+
+# Solve hCaptcha
+result = HCaptcha(aptcha_key=key).captcha_handler(site_url="https://example.com", site_key="abc123")
+
+if result['code'] == 0:
+ print(f"Solved! Token: {result['token']}")
+else:
+ print(f"Error: {result['message']}")
+```
+
+### Solving Different CAPTCHA Types
+
+**reCAPTCHA v2:**
+```python
+from python_rucaptcha import ReCaptcha
+
+result = ReCaptcha(api_key).captcha_handler(
+ site_url="https://example.com",
+ site_key="your_site_key"
+)
+```
+
+**Image CAPTCHA:**
+```python
+from python_rucaptcha import ImageCaptcha
+
+result = ImageCaptcha(api_key).captcha_handler(
+ image_link="https://example.com/captcha.jpg"
+)
+```
+
+**ALTCHA:**
+```python
+from python_rucaptcha import AltchaCaptcha
+from python_rucaptcha.core.enums import AltchaEnm
+
+result = AltchaCaptcha(
+ rucaptcha_key=api_key,
+ websiteURL="https://example.com",
+ challengeURL="https://example.com/altcha/challenge",
+ method=AltchaEnm.AltchaTaskProxyless,
+).captcha_handler()
+```
+
+**Using async:**
+```python
+import asyncio
+from python_rucaptcha import HCaptcha
+
+async def solve():
+ result = await HCaptcha(api_key).aio_captcha_handler(
+ site_url="https://example.com",
+ site_key="abc123"
+ )
+ return result
+
+token = asyncio.run(solve())
```
+## Supported CAPTCHA Types
-## How to use?
+| CAPTCHA | Module | Description |
+|---------|--------|-------------|
+| reCAPTCHA v2/v3 | `ReCaptcha` | Google reCAPTCHA |
+| hCaptcha | `HCaptcha` | hCaptcha challenge |
+| Cloudflare Turnstile | `Turnstile` | Cloudflare protection |
+| Image | `ImageCaptcha` | Type the text from image |
+| Audio | `AudioCaptcha` | Listen and type audio |
+| GeeTest | `GeeTest` | Chinese geetest puzzles |
+| KeyCaptcha | `KeyCaptcha` | KeyCAPTCHA service |
+| Amazon WAF | `AmazonWaf` | AWS WAF challenge |
+| ALTCHA | `AltchaCaptcha` | ALTCHA challenge |
+| Binance | `BinanceCaptcha` | Token-based Binance challenge |
+| Grid | `GridCaptcha` | Select grid cells |
+| Coordinates | `CoordinatesCaptcha` | Click on coordinates |
+| And 20+ more | ... | See [full docs](https://andreidrang.github.io/python-rucaptcha/) |
-Is described in the [documentation-website](https://andreidrang.github.io/python-rucaptcha/).
+## Switching Services
-## How to test?
+Use the same code with different services:
-1. You need set ``RUCAPTCHA_KEY`` in your environment(get this value from you account).
-2. Run command ``make tests``, from root directory.
+```python
+from python_rucaptcha import HCaptcha
+from python_rucaptcha.core.enums import ServiceEnm
+# Use 2Captcha (default)
+result = HCaptcha("2captcha_key").captcha_handler(...)
+
+# Use RuCaptcha
+result = HCaptcha("rucaptcha_key", service_type=ServiceEnm.RuCaptcha).captcha_handler(...)
+
+# Use DeathByCaptcha
+result = HCaptcha("dbc_user:dbc_pass", service_type=ServiceEnm.DeathByCaptcha).captcha_handler(...)
+```
+
+## Testing
+
+```bash
+# Set your API key
+export RUCAPTCHA_KEY="your_key_here"
+
+# Run tests
+make tests
+```
-### Changelog
+## Documentation
-For full changelog info check - [Releases page](https://github.com/AndreiDrang/python-rucaptcha/releases).
+For advanced usage, configuration options, and all CAPTCHA types, see the [full documentation](https://andreidrang.github.io/python-rucaptcha/).
-- v.6.0 - Library refactoring. Stop using `pydantic`, start using `msgspec`. Move to API v2. Drop Python 3.8 support. More details at [Releases page](https://github.com/AndreiDrang/python-rucaptcha/releases).
-- v.5.3 - Added support for [Death By Captcha](https://www.deathbycaptcha.com?refid=1237267242) and other services by changing `service_type` and `url_request` \ `url_response` parameters.
-- v.5.2 - Added Audio captcha method.
-- v.5.1 - Check [releases page](https://github.com/AndreiDrang/python-rucaptcha/releases).
-- v.5.0 - Added AmazonWAF captcha method.
-- v.4.2 - Added [Yandex Smart Captcha](https://rucaptcha.com/api-rucaptcha#yandex).
+## Support
-### Get API Key to work with the library
-1. On the page - https://rucaptcha.com/enterpage
-2. Find it: [](https://vyjava.xyz/dashboard/image/ac679557-f3cc-402f-bf95-6c45d252a2ef)
+- **Telegram:** [pythoncaptcha](https://t.me/pythoncaptcha)
+- **Email:** python-captcha@pm.me
+- **Issues:** [GitHub Issues](https://github.com/AndreiDrang/python-rucaptcha/issues)
-### Contacts
+## Changelog
-If you have any questions, please send a message to the [Telegram](https://t.me/pythoncaptcha) chat room.
+See [Releases](https://github.com/AndreiDrang/python-rucaptcha/releases) for full changelog.
-Or email python-captcha@pm.me
+- **v6.0** - Refactored to use msgspec (faster), API v2, dropped Python 3.8
+- **v5.3** - Added DeathByCaptcha support
+- **v5.2** - Added audio CAPTCHA solving
diff --git a/docs/conf.py b/docs/conf.py
index 8d3f987a..38108420 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -25,7 +25,9 @@
text_captcha,
image_captcha,
lemin_captcha,
+ yidun_captcha,
rotate_captcha,
+ binance_captcha,
datadome_captcha,
friendly_captcha,
cyber_siara_captcha,
diff --git a/docs/index.rst b/docs/index.rst
index 586851dd..a26664d5 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -52,9 +52,12 @@ Check our other projects here - `RedPandaDev group =2.32.4 # not directly required, pinned by Snyk to avoid a vulnerability
urllib3>=2.2.2 # not directly required, pinned by Snyk to avoid a vulnerability
-zipp>=3.19.1 # not directly required, pinned by Snyk to avoid a vulnerability
+zipp>=3.23.1 # not directly required, pinned by Snyk to avoid a vulnerability
+idna>=3.15 # not directly required, pinned by Snyk to avoid a vulnerability
diff --git a/pyproject.toml b/pyproject.toml
index 8175c19c..24ff62cc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -69,7 +69,8 @@ keywords = [ "captcha",
"vk-captcha",
"fox-captcha",
"temu-captcha",
- "friendly-captcha"
+ "friendly-captcha",
+ "binance-captcha"
]
license = "MIT"
classifiers = [
@@ -90,7 +91,7 @@ classifiers = [
dependencies = [
"requests>=2.21.0",
"aiohttp>=3.9.2",
- "msgspec>=0.18,<0.20",
+ "msgspec>=0.18,<0.21",
"tenacity>=8,<10"
]
diff --git a/requirements.style.txt b/requirements.style.txt
index ad634e98..b4100bfa 100644
--- a/requirements.style.txt
+++ b/requirements.style.txt
@@ -1,4 +1,4 @@
# codestyle
isort==6.*
-black==25.9.0
+black==25.11.0
autoflake==2.*
diff --git a/src/python_rucaptcha/AGENTS.md b/src/python_rucaptcha/AGENTS.md
new file mode 100644
index 00000000..77569d04
--- /dev/null
+++ b/src/python_rucaptcha/AGENTS.md
@@ -0,0 +1,46 @@
+# python_rucaptcha Package
+
+**Parent:** ./AGENTS.md
+
+## OVERVIEW
+Main package containing 30+ CAPTCHA solver modules. Each module implements a specific CAPTCHA type.
+
+## STRUCTURE
+```
+src/python_rucaptcha/
+├── __init__.py # Exports __version__, package info
+├── core/ # Base classes (base.py, config.py, enums.py, serializer.py)
+├── _captcha.py # Internal helpers
+├── hcaptcha.py # hCaptcha solver
+├── re_captcha.py # reCaptcha v2/v3 solver
+├── turnstile.py # Cloudflare Turnstile solver
+├── image_captcha.py # Image captcha solver
+├── audio_captcha.py # Audio captcha solver
+├── gee_test.py # GeeTest solver
+├── key_captcha.py # KeyCaptcha solver
+├── ... # 20+ more captcha types
+└── control.py # Balance/status checker
+```
+
+## WHERE TO LOOK
+| Task | File |
+|------|------|
+| Add new CAPTCHA type | Create new `*_captcha.py` module |
+| Modify BaseCaptcha | `core/base.py` |
+| Add new enum | `core/enums.py` |
+
+## CONVENTIONS
+- **Module naming**: `*_captcha.py` pattern
+- **Class naming**: `{CaptchaType}Captcha` (e.g., `HCaptcha`, `ReCaptcha`)
+- **Enum naming**: `{CaptchaType}Enm` (e.g., `HCaptchaEnm`)
+- **Inheritance**: All captcha classes extend `BaseCaptcha`
+
+## ADDING NEW CAPTCHA
+1. Create `src/python_rucaptcha/new_captcha.py`
+2. Define enum `NewCaptchaEnm` in `core/enums.py`
+3. Create class `NewCaptcha(BaseCaptcha)` implementing required methods
+4. Add tests in `tests/test_new_captcha.py`
+
+## ANTI-PATTERNS
+- DO NOT modify core files without understanding the inheritance chain
+- DO NOT add captcha-specific logic directly in BaseCaptcha
diff --git a/src/python_rucaptcha/__version__.py b/src/python_rucaptcha/__version__.py
index b61ecefd..e205487d 100644
--- a/src/python_rucaptcha/__version__.py
+++ b/src/python_rucaptcha/__version__.py
@@ -1 +1 @@
-__version__ = "6.5.0"
+__version__ = "6.6.0"
diff --git a/src/python_rucaptcha/altcha_captcha.py b/src/python_rucaptcha/altcha_captcha.py
new file mode 100644
index 00000000..174409f4
--- /dev/null
+++ b/src/python_rucaptcha/altcha_captcha.py
@@ -0,0 +1,152 @@
+from typing import Union, Optional
+
+from .core.base import BaseCaptcha
+from .core.enums import AltchaEnm
+
+
+class AltchaCaptcha(BaseCaptcha):
+ def __init__(
+ self,
+ websiteURL: str,
+ method: Union[str, AltchaEnm] = AltchaEnm.AltchaTaskProxyless,
+ challengeURL: Optional[str] = None,
+ challengeJSON: Optional[str] = None,
+ proxyType: Optional[str] = None,
+ proxyAddress: Optional[str] = None,
+ proxyPort: Optional[int] = None,
+ proxyLogin: Optional[str] = None,
+ proxyPassword: Optional[str] = None,
+ *args,
+ **kwargs,
+ ):
+ """
+ The class is used to work with ALTCHA captcha.
+
+ Args:
+ rucaptcha_key: User API key
+ websiteURL: Full URL of the captcha page
+ method: Captcha type
+ challengeURL: Full URL of the page that contains ALTCHA challenge
+ challengeJSON: JSON-encoded ALTCHA challenge data
+ proxyType: Proxy type (http, https, socks4, socks5)
+ proxyAddress: Proxy IP address or hostname
+ proxyPort: Proxy port
+ proxyLogin: Proxy login
+ proxyPassword: Proxy password
+ kwargs: Not required params for task creation request
+
+ Examples:
+ >>> AltchaCaptcha(rucaptcha_key="aa9011f31111181111168611f1151122",
+ ... websiteURL="https://example.com",
+ ... challengeURL="https://example.com/altcha/challenge.js",
+ ... method=AltchaEnm.AltchaTaskProxyless.value,
+ ... ).captcha_handler()
+ {
+ "errorId":0,
+ "status":"ready",
+ "solution":{
+ "token":"..."
+ },
+ "cost":"0.00145",
+ "ip":"1.2.3.4",
+ "createTime":1692863536,
+ "endTime":1692863556,
+ "solveCount":1,
+ "taskId": 73243152973,
+ }
+
+ >>> await AltchaCaptcha(rucaptcha_key="aa9011f31111181111168611f1151122",
+ ... websiteURL="https://example.com",
+ ... challengeJSON='{"挑战数据"}',
+ ... method=AltchaEnm.AltchaTask.value,
+ ... proxyType="http",
+ ... proxyAddress="1.2.3.4",
+ ... proxyPort=8080,
+ ... ).aio_captcha_handler()
+ {
+ "errorId":0,
+ "status":"ready",
+ "solution":{
+ "token":"..."
+ },
+ "cost":"0.00145",
+ "ip":"1.2.3.4",
+ "createTime":1692863536,
+ "endTime":1692863556,
+ "solveCount":1,
+ "taskId": 73243152973,
+ }
+
+ Returns:
+ Dict with full server response
+
+ Notes:
+ https://rucaptcha.com/api-docs/altcha
+ """
+
+ super().__init__(method=method, *args, **kwargs)
+
+ # XOR validation: exactly one of challengeURL or challengeJSON must be provided
+ if not (bool(challengeURL) ^ bool(challengeJSON)):
+ raise ValueError(
+ "Exactly one of 'challengeURL' or 'challengeJSON' must be provided, not both or neither"
+ )
+
+ # Validate method
+ if method not in AltchaEnm.list_values():
+ raise ValueError(f"Invalid method parameter set, available - {AltchaEnm.list_values()}")
+
+ # Build task payload
+ task_data = {"websiteURL": websiteURL}
+
+ if challengeURL:
+ task_data["challengeURL"] = challengeURL
+
+ if challengeJSON:
+ task_data["challengeJSON"] = challengeJSON
+
+ # Add proxy params only for non-proxyless methods
+ if method == AltchaEnm.AltchaTask.value:
+ if not all([proxyType, proxyAddress, proxyPort]):
+ raise ValueError(
+ "Proxy parameters (proxyType, proxyAddress, proxyPort) are required for AltchaTask"
+ )
+ task_data.update(
+ {
+ "proxyType": proxyType,
+ "proxyAddress": proxyAddress,
+ "proxyPort": proxyPort,
+ }
+ )
+ if proxyLogin and proxyPassword:
+ task_data["proxyLogin"] = proxyLogin
+ task_data["proxyPassword"] = proxyPassword
+
+ self.create_task_payload["task"].update(task_data)
+
+ def captcha_handler(self, **kwargs) -> dict:
+ """
+ Sync solving method
+
+ Args:
+ kwargs: Parameters for the `requests` library
+
+ Returns:
+ Dict with full server response
+
+ Notes:
+ Check class docstirng for more info
+ """
+ return self._processing_response(**kwargs)
+
+ async def aio_captcha_handler(self) -> dict:
+ """
+ Async solving method
+
+ Returns:
+ Dict with full server response
+
+ Notes:
+ Check class docstirng for more info
+ """
+ return await self._aio_processing_response()
diff --git a/src/python_rucaptcha/binance_captcha.py b/src/python_rucaptcha/binance_captcha.py
new file mode 100644
index 00000000..d925b11b
--- /dev/null
+++ b/src/python_rucaptcha/binance_captcha.py
@@ -0,0 +1,155 @@
+from typing import Union, Optional
+
+from .core.base import BaseCaptcha
+from .core.enums import BinanceCaptchaEnm
+
+
+class BinanceCaptcha(BaseCaptcha):
+ def __init__(
+ self,
+ websiteURL: str,
+ websiteKey: str,
+ validateId: str,
+ method: Union[str, BinanceCaptchaEnm] = BinanceCaptchaEnm.BinanceTaskProxyless,
+ userAgent: Optional[str] = None,
+ proxyType: Optional[str] = None,
+ proxyAddress: Optional[str] = None,
+ proxyPort: Optional[int] = None,
+ proxyLogin: Optional[str] = None,
+ proxyPassword: Optional[str] = None,
+ *args,
+ **kwargs,
+ ):
+ """
+ The class is used to work with Binance CAPTCHA.
+
+ Args:
+ rucaptcha_key: User API key
+ websiteURL: Full URL of the page where the captcha is loaded
+ websiteKey: Value of bizId, bizType, or bizCode from page requests
+ validateId: Dynamic value of validateId, securityId, or securityCheckResponseValidateId
+ method: Captcha type
+ userAgent: User-Agent string to be used when solving the captcha
+ proxyType: Proxy type (http, https, socks4, socks5)
+ proxyAddress: Proxy IP address or hostname
+ proxyPort: Proxy port
+ proxyLogin: Proxy login
+ proxyPassword: Proxy password
+
+ Examples:
+ >>> BinanceCaptcha(rucaptcha_key="aa9011f31111181111168611f1151122",
+ ... websiteURL="https://example.com/page-with-binance",
+ ... websiteKey="login",
+ ... validateId="cb0bfefa598...e54ecd57b",
+ ... userAgent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
+ ... "(KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
+ ... method=BinanceCaptchaEnm.BinanceTaskProxyless.value,
+ ... ).captcha_handler()
+ {
+ "errorId":0,
+ "status":"ready",
+ "solution":{
+ "token":"captcha#09ba4905a79f44f...kc99maS943qIsquNP9D77",
+ "userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
+ },
+ "cost":"0.00299",
+ "ip":"1.2.3.4",
+ "createTime":1692863536,
+ "endTime":1692863556,
+ "solveCount":1,
+ "taskId": 73243152973,
+ }
+
+ >>> await BinanceCaptcha(rucaptcha_key="aa9011f31111181111168611f1151122",
+ ... websiteURL="https://example.com/page-with-binance",
+ ... websiteKey="login",
+ ... validateId="cb0bfefa598...e54ecd57b",
+ ... method=BinanceCaptchaEnm.BinanceTask.value,
+ ... proxyType="http",
+ ... proxyAddress="1.2.3.4",
+ ... proxyPort=8080,
+ ... ).aio_captcha_handler()
+ {
+ "errorId":0,
+ "status":"ready",
+ "solution":{
+ "token":"captcha#09ba4905a79f44f...kc99maS943qIsquNP9D77",
+ "userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
+ },
+ "cost":"0.00299",
+ "ip":"1.2.3.4",
+ "createTime":1692863536,
+ "endTime":1692863556,
+ "solveCount":1,
+ "taskId": 73243152973,
+ }
+
+ Returns:
+ Dict with full server response
+
+ Notes:
+ https://2captcha.com/api-docs/binance-captcha
+
+ https://rucaptcha.com/api-docs/binance-captcha
+ """
+ super().__init__(method=method, *args, **kwargs)
+
+ # Validate method
+ if method not in BinanceCaptchaEnm.list_values():
+ raise ValueError(f"Invalid method parameter set, available - {BinanceCaptchaEnm.list_values()}")
+
+ # Build task payload
+ task_data = {
+ "websiteURL": websiteURL,
+ "websiteKey": websiteKey,
+ "validateId": validateId,
+ }
+
+ if userAgent:
+ task_data["userAgent"] = userAgent
+
+ # Add proxy params only for non-proxyless methods
+ if method == BinanceCaptchaEnm.BinanceTask.value:
+ if not all([proxyType, proxyAddress, proxyPort]):
+ raise ValueError(
+ "Proxy parameters (proxyType, proxyAddress, proxyPort) are required for BinanceTask"
+ )
+ task_data.update(
+ {
+ "proxyType": proxyType,
+ "proxyAddress": proxyAddress,
+ "proxyPort": proxyPort,
+ }
+ )
+ if proxyLogin and proxyPassword:
+ task_data["proxyLogin"] = proxyLogin
+ task_data["proxyPassword"] = proxyPassword
+
+ self.create_task_payload["task"].update(task_data)
+
+ def captcha_handler(self, **kwargs) -> dict:
+ """
+ Sync solving method
+
+ Args:
+ kwargs: Parameters for the `requests` library
+
+ Returns:
+ Dict with full server response
+
+ Notes:
+ Check class docstirng for more info
+ """
+ return self._processing_response(**kwargs)
+
+ async def aio_captcha_handler(self) -> dict:
+ """
+ Async solving method
+
+ Returns:
+ Dict with full server response
+
+ Notes:
+ Check class docstirng for more info
+ """
+ return await self._aio_processing_response()
diff --git a/src/python_rucaptcha/core/AGENTS.md b/src/python_rucaptcha/core/AGENTS.md
new file mode 100644
index 00000000..580836be
--- /dev/null
+++ b/src/python_rucaptcha/core/AGENTS.md
@@ -0,0 +1,25 @@
+# core Module
+
+**Parent:** ../AGENTS.md, ../../src/python_rucaptcha/AGENTS.md
+
+## OVERVIEW
+Foundation module with base classes, configuration, serialization, and enums. All captcha solvers depend on these.
+
+## WHERE TO LOOK
+| File | Role |
+|------|------|
+| `base.py` | BaseCaptcha class - parent for all solvers |
+| `config.py` | CaptchaOptionsSer - service URL abstraction |
+| `serializer.py` | MyBaseModel - msgspec wrapper |
+| `enums.py` | MyEnum + 25+ CAPTCHA enums |
+
+## KEY SYMBOLS
+- `BaseCaptcha`: Parent class for all captcha solvers (sync + async)
+- `MyBaseModel`: msgspec Struct wrapper with `.to_dict()`
+- `CaptchaOptionsSer`: Dynamic service URL selection
+- `MyEnum`: Custom enum with `.list()`, `.list_values()`, `.list_names()`
+
+## CONVENTIONS
+- **BaseCaptcha provides**: `captcha_handler()`, `aio_captcha_handler()`, session management, file handling
+- **Serialization**: Uses msgspec (not pydantic)
+- **Enums**: Each CAPTCHA type has dedicated enum in enums.py
diff --git a/src/python_rucaptcha/core/enums.py b/src/python_rucaptcha/core/enums.py
index 544be6e6..26315bff 100644
--- a/src/python_rucaptcha/core/enums.py
+++ b/src/python_rucaptcha/core/enums.py
@@ -98,6 +98,11 @@ class TurnstileCaptchaEnm(str, MyEnum):
TurnstileTask = "TurnstileTask"
+class AltchaEnm(str, MyEnum):
+ AltchaTaskProxyless = "AltchaTaskProxyless"
+ AltchaTask = "AltchaTask"
+
+
class AmazonWAFCaptchaEnm(str, MyEnum):
AmazonTask = "AmazonTask"
AmazonTaskProxyless = "AmazonTaskProxyless"
@@ -172,7 +177,23 @@ class CaptchaFoxEnm(str, MyEnum):
class VKCaptchaEnm(str, MyEnum):
VKCaptchaTask = "VKCaptchaTask"
+ VKCaptchaImageTask = "VKCaptchaImageTask"
class TemuCaptchaEnm(str, MyEnum):
TemuCaptchaTask = "TemuCaptchaTask"
+
+
+class BinanceCaptchaEnm(str, MyEnum):
+ BinanceTaskProxyless = "BinanceTaskProxyless"
+ BinanceTask = "BinanceTask"
+
+
+class YidunEnm(str, MyEnum):
+ YidunTaskProxyless = "YidunTaskProxyless"
+ YidunTask = "YidunTask"
+
+
+class YandexSmartCaptchaEnm(str, MyEnum):
+ YandexSmartCaptchaTaskProxyless = "YandexSmartCaptchaTaskProxyless"
+ YandexSmartCaptchaTask = "YandexSmartCaptchaTask"
diff --git a/src/python_rucaptcha/vk_captcha.py b/src/python_rucaptcha/vk_captcha.py
index 23d9f6a8..ff76f763 100644
--- a/src/python_rucaptcha/vk_captcha.py
+++ b/src/python_rucaptcha/vk_captcha.py
@@ -1,32 +1,43 @@
-from typing import Any
+from typing import Any, Union, Optional
from .core.base import BaseCaptcha
-from .core.enums import VKCaptchaEnm
+from .core.enums import VKCaptchaEnm, SaveFormatsEnm
class VKCaptcha(BaseCaptcha):
def __init__(
self,
- redirectUri: str,
- userAgent: str,
- proxyType: str,
- proxyAddress: str,
- proxyPort: str,
+ method: Union[str, VKCaptchaEnm] = VKCaptchaEnm.VKCaptchaTask,
+ redirectUri: Optional[str] = None,
+ userAgent: Optional[str] = None,
+ proxyType: Optional[str] = None,
+ proxyAddress: Optional[str] = None,
+ proxyPort: Optional[int] = None,
+ proxyLogin: Optional[str] = None,
+ proxyPassword: Optional[str] = None,
+ save_format: Union[str, SaveFormatsEnm] = SaveFormatsEnm.TEMP,
+ img_path: str = "PythonRuCaptchaImages",
*args,
- **kwargs: dict[str, Any],
+ **kwargs,
):
"""
- The class is used to work with VKCaptchaTask.
+ The class is used to work with VKCaptchaTask and VKCaptchaImageTask.
+
+ VKCaptchaTask requires proxy and returns a token.
+ VKCaptchaImageTask takes a captcha image and steps, returns image solution.
Args:
rucaptcha_key: User API key
- redirectUri: The URL that is returned on requests to the captcha API.
- userAgent: User-Agent of your browser will be used to load the captcha.
- Use only modern browser's User-Agents
- proxyType: Proxy type - `http`, `socks4`, `socks5`
- proxyAddress: Proxy IP address or hostname
- proxyPort: Proxy port
- method: Captcha type
+ method: Captcha type - VKCaptchaTask or VKCaptchaImageTask
+ redirectUri: The URL that is returned on requests to the captcha API (VKCaptchaTask)
+ userAgent: User-Agent of your browser will be used to load the captcha (VKCaptchaTask)
+ proxyType: Proxy type - http, https, socks5 (VKCaptchaTask)
+ proxyAddress: Proxy IP address or hostname (VKCaptchaTask)
+ proxyPort: Proxy port (VKCaptchaTask)
+ proxyLogin: Proxy login (VKCaptchaTask)
+ proxyPassword: Proxy password (VKCaptchaTask)
+ save_format: Image save format for VKCaptchaImageTask - 'temp' or 'const'
+ img_path: Folder to save captcha images for VKCaptchaImageTask
kwargs: Not required params for task creation request
Examples:
@@ -35,7 +46,7 @@ def __init__(
... userAgent="Mozilla/5.0 .....",
... proxyType="socks5",
... proxyAddress="1.2.3.4",
- ... proxyPort="445",
+ ... proxyPort=445,
... ).captcha_handler()
{
"errorId":0,
@@ -56,7 +67,7 @@ def __init__(
... userAgent="Mozilla/5.0 .....",
... proxyType="socks5",
... proxyAddress="1.2.3.4",
- ... proxyPort="445",
+ ... proxyPort=445,
... ).aio_captcha_handler()
{
"errorId":0,
@@ -72,6 +83,28 @@ def __init__(
"taskId": 73243152973,
}
+ >>> VKCaptcha(rucaptcha_key="aa9011f31111181111168611f1151122",
+ ... method=VKCaptchaEnm.VKCaptchaImageTask,
+ ... ).captcha_handler(
+ ... captcha_link="https://example.com/vk_captcha.png", steps=[3, 4, 5]
+ ... )
+ {
+ "errorId":0,
+ "status":"ready",
+ "solution":{
+ "best_step": 1,
+ "preview": "...",
+ "solution": "...",
+ "answer": "..."
+ },
+ "cost":"0.002",
+ "ip":"1.2.3.4",
+ "createTime":1692863536,
+ "endTime":1692863556,
+ "solveCount":0,
+ "taskId": 73243152973,
+ }
+
Returns:
Dict with full server response
@@ -80,23 +113,51 @@ def __init__(
https://rucaptcha.com/api-docs/vk-captcha
"""
- super().__init__(method=VKCaptchaEnm.VKCaptchaTask, *args, **kwargs)
+ super().__init__(method=method, *args, **kwargs)
- self.create_task_payload["task"].update(
- {
- "websiteURL": redirectUri,
+ if method not in VKCaptchaEnm.list_values():
+ raise ValueError(f"Invalid method parameter set, available - {VKCaptchaEnm.list_values()}")
+
+ self.method = method
+ self.save_format = save_format
+ self.img_path = img_path
+
+ if method == VKCaptchaEnm.VKCaptchaTask:
+ if not all([redirectUri, userAgent, proxyType, proxyAddress, proxyPort]):
+ raise ValueError(
+ "redirectUri, userAgent, proxyType, proxyAddress, "
+ "and proxyPort are required for VKCaptchaTask"
+ )
+
+ task_data = {
+ "redirectUri": redirectUri,
"userAgent": userAgent,
"proxyType": proxyType,
"proxyAddress": proxyAddress,
"proxyPort": proxyPort,
}
- )
+ if proxyLogin and proxyPassword:
+ task_data["proxyLogin"] = proxyLogin
+ task_data["proxyPassword"] = proxyPassword
+
+ self.create_task_payload["task"].update(task_data)
- def captcha_handler(self, **kwargs: dict[str, Any]) -> dict[str, Any]:
+ def captcha_handler(
+ self,
+ captcha_link: Optional[str] = None,
+ captcha_file: Optional[str] = None,
+ captcha_base64: Optional[bytes] = None,
+ steps: Optional[list[int]] = None,
+ **kwargs: dict[str, Any],
+ ) -> dict[str, Any]:
"""
Sync solving method
Args:
+ captcha_link: Captcha image URL (VKCaptchaImageTask)
+ captcha_file: Captcha image file path (VKCaptchaImageTask)
+ captcha_base64: Captcha image BASE64 info (VKCaptchaImageTask)
+ steps: List of step values for VKCaptchaImageTask
kwargs: additional params for `requests` library
Returns:
@@ -105,16 +166,60 @@ def captcha_handler(self, **kwargs: dict[str, Any]) -> dict[str, Any]:
Notes:
Check class docstirng for more info
"""
+ if self.method == VKCaptchaEnm.VKCaptchaImageTask:
+ self._body_file_processing(
+ save_format=self.save_format,
+ file_path=self.img_path,
+ image_key="image",
+ captcha_link=captcha_link,
+ captcha_file=captcha_file,
+ captcha_base64=captcha_base64,
+ )
+ if steps:
+ self.create_task_payload["task"]["steps"] = steps
+ if not self.result.errorId:
+ return self._processing_response(**kwargs)
+ return self.result.to_dict()
+
return self._processing_response(**kwargs)
- async def aio_captcha_handler(self) -> dict[str, Any]:
+ async def aio_captcha_handler(
+ self,
+ captcha_link: Optional[str] = None,
+ captcha_file: Optional[str] = None,
+ captcha_base64: Optional[bytes] = None,
+ steps: Optional[list[int]] = None,
+ **kwargs: dict[str, Any],
+ ) -> dict[str, Any]:
"""
Async solving method
+ Args:
+ captcha_link: Captcha image URL (VKCaptchaImageTask)
+ captcha_file: Captcha image file path (VKCaptchaImageTask)
+ captcha_base64: Captcha image BASE64 info (VKCaptchaImageTask)
+ steps: List of step values for VKCaptchaImageTask
+ kwargs: additional params for `aiohttp` library
+
Returns:
Dict with full server response
Notes:
Check class docstirng for more info
"""
+ if self.method == VKCaptchaEnm.VKCaptchaImageTask:
+ await self._aio_body_file_processing(
+ save_format=self.save_format,
+ file_path=self.img_path,
+ image_key="image",
+ captcha_link=captcha_link,
+ captcha_file=captcha_file,
+ captcha_base64=captcha_base64,
+ )
+ if steps:
+ self.create_task_payload["task"]["steps"] = steps
+ if not self.result.errorId:
+ return await self._aio_processing_response()
+ return self.result.to_dict()
+
return await self._aio_processing_response()
diff --git a/src/python_rucaptcha/yandex_smart_captcha.py b/src/python_rucaptcha/yandex_smart_captcha.py
new file mode 100644
index 00000000..09522671
--- /dev/null
+++ b/src/python_rucaptcha/yandex_smart_captcha.py
@@ -0,0 +1,246 @@
+import shutil
+from typing import Any, Union, Optional
+
+from .core.base import BaseCaptcha
+from .core.enums import (
+ SaveFormatsEnm,
+ CoordinatesCaptchaEnm,
+ YandexSmartCaptchaEnm,
+)
+
+
+class YandexSmartCaptcha(BaseCaptcha):
+ """
+ The class is used to work with Yandex SmartCaptcha.
+
+ Supports three 2Captcha task types:
+ - YandexSmartCaptchaTaskProxyless (token, no proxy)
+ - YandexSmartCaptchaTask (token, user proxy)
+ - CoordinatesTask (image, two imgType modes: smart_captcha, pazl_smart_captcha)
+
+ Args:
+ rucaptcha_key: User API key
+ websiteURL: Full URL of the page where the captcha is loaded (token methods)
+ websiteKey: Sitekey from the page source (token methods)
+ method: Captcha type. Default YandexSmartCaptchaTaskProxyless.
+ userAgent: Browser User-Agent to use (token methods, optional)
+ cookies: Cookies to send in the request (token methods, optional, format "name1=value1;name2=value2")
+ proxyType: Proxy type (http, https, socks4, socks5) - required for YandexSmartCaptchaTask
+ proxyAddress: Proxy IP/hostname - required for YandexSmartCaptchaTask
+ proxyPort: Proxy port - required for YandexSmartCaptchaTask
+ proxyLogin: Proxy login (optional)
+ proxyPassword: Proxy password (optional)
+ imgType: Image variant type - "smart_captcha" or "pazl_smart_captcha" (CoordinatesTask only)
+ comment: Text hint for the worker - required for imgType="smart_captcha"
+ save_format: How to save the image - "temp" or "const" (CoordinatesTask only)
+ img_clearing: Whether to delete the image folder on instance destruction
+ img_path: Folder name for saved images
+
+ Examples:
+ >>> YandexSmartCaptcha(
+ ... rucaptcha_key="aa9011f31111181111168611f1151122",
+ ... websiteURL="https://example.com/",
+ ... websiteKey="Y5Lh0ti...",
+ ... ).captcha_handler()
+ {"errorId": 0, "status": "ready", "solution": {"token": "..."}, "taskId": ...}
+
+ >>> await YandexSmartCaptcha(
+ ... rucaptcha_key="aa9011f31111181111168611f1151122",
+ ... websiteURL="https://example.com/",
+ ... websiteKey="Y5Lh0ti...",
+ ... method=YandexSmartCaptchaEnm.YandexSmartCaptchaTask,
+ ... proxyType="http",
+ ... proxyAddress="1.2.3.4",
+ ... proxyPort=8080,
+ ... ).aio_captcha_handler()
+ {"errorId": 0, "status": "ready", "solution": {"token": "..."}, "taskId": ...}
+
+ >>> YandexSmartCaptcha(
+ ... rucaptcha_key="aa9011f31111181111168611f1151122",
+ ... method=CoordinatesCaptchaEnm.CoordinatesTask,
+ ... imgType="smart_captcha",
+ ... comment="select objects in the order of the instruction",
+ ... ).captcha_handler(
+ ... captcha_file="src/examples/088636.png",
+ ... imgInstructions_file="src/examples/bounding_box_start.png",
+ ... )
+ {"errorId": 0, "status": "ready", "solution": {"coordinates": [{"x": 57, "y": 82}, ...]}, "taskId": ...}
+
+ Notes:
+ https://2captcha.com/api-docs/yandex-smart-captcha
+
+ https://rucaptcha.com/api-docs/yandex-smart-captcha
+ """
+
+ _VALID_METHODS = YandexSmartCaptchaEnm.list_values() + [CoordinatesCaptchaEnm.CoordinatesTask.value]
+
+ def __init__(
+ self,
+ websiteURL: Optional[str] = None,
+ websiteKey: Optional[str] = None,
+ method: Union[str, YandexSmartCaptchaEnm, CoordinatesCaptchaEnm] = (
+ YandexSmartCaptchaEnm.YandexSmartCaptchaTaskProxyless
+ ),
+ userAgent: Optional[str] = None,
+ cookies: Optional[str] = None,
+ proxyType: Optional[str] = None,
+ proxyAddress: Optional[str] = None,
+ proxyPort: Optional[int] = None,
+ proxyLogin: Optional[str] = None,
+ proxyPassword: Optional[str] = None,
+ imgType: Optional[str] = None,
+ comment: Optional[str] = None,
+ save_format: Union[str, SaveFormatsEnm] = SaveFormatsEnm.TEMP,
+ img_clearing: bool = True,
+ img_path: str = "PythonRuCaptchaYandexSmart",
+ *args,
+ **kwargs,
+ ):
+ method_str = method.value if hasattr(method, "value") else method
+
+ if method_str not in self._VALID_METHODS:
+ raise ValueError(f"Invalid method parameter set, available - {self._VALID_METHODS}")
+
+ is_token = method_str in YandexSmartCaptchaEnm.list_values()
+ is_image = method_str == CoordinatesCaptchaEnm.CoordinatesTask.value
+
+ # token-method-specific validation
+ if is_token:
+ if not (websiteURL and websiteKey):
+ raise ValueError(
+ "websiteURL and websiteKey are required for token methods "
+ f"({YandexSmartCaptchaEnm.list_values()})"
+ )
+
+ # proxy-method-specific validation
+ if method_str == YandexSmartCaptchaEnm.YandexSmartCaptchaTask.value:
+ if not all([proxyType, proxyAddress, proxyPort]):
+ raise ValueError(
+ "proxyType, proxyAddress, and proxyPort are required for YandexSmartCaptchaTask"
+ )
+
+ # image-method-specific validation
+ if is_image:
+ if not imgType:
+ raise ValueError("imgType is required for CoordinatesTask")
+ if imgType == "smart_captcha" and not comment:
+ raise ValueError('comment is required for CoordinatesTask with imgType="smart_captcha"')
+
+ # Build task payload
+ task_data: dict[str, Any] = {}
+ if is_token:
+ task_data["websiteURL"] = websiteURL
+ task_data["websiteKey"] = websiteKey
+ if userAgent is not None:
+ task_data["userAgent"] = userAgent
+ if cookies is not None:
+ task_data["cookies"] = cookies
+ if method_str == YandexSmartCaptchaEnm.YandexSmartCaptchaTask.value:
+ task_data["proxyType"] = proxyType
+ task_data["proxyAddress"] = proxyAddress
+ task_data["proxyPort"] = proxyPort
+ if proxyLogin is not None:
+ task_data["proxyLogin"] = proxyLogin
+ if proxyPassword is not None:
+ task_data["proxyPassword"] = proxyPassword
+ elif is_image:
+ task_data["imgType"] = imgType
+ if comment is not None:
+ task_data["comment"] = comment
+
+ super().__init__(method=method, *args, **kwargs)
+ self.method = method_str
+ self.save_format = save_format
+ self.img_clearing = img_clearing
+ self.img_path = img_path
+ self.create_task_payload["task"].update(task_data)
+
+ def captcha_handler(
+ self,
+ captcha_link: Optional[str] = None,
+ captcha_file: Optional[str] = None,
+ captcha_base64: Optional[bytes] = None,
+ imgInstructions_link: Optional[str] = None,
+ imgInstructions_file: Optional[str] = None,
+ imgInstructions_base64: Optional[bytes] = None,
+ **kwargs: dict[str, Any],
+ ) -> dict[str, Any]:
+ """
+ Sync solving method.
+ """
+ if self.method == CoordinatesCaptchaEnm.CoordinatesTask.value:
+ self._body_file_processing(
+ save_format=self.save_format,
+ file_path=self.img_path,
+ image_key="body",
+ captcha_link=captcha_link,
+ captcha_file=captcha_file,
+ captcha_base64=captcha_base64,
+ **kwargs,
+ )
+ if any([imgInstructions_link, imgInstructions_file, imgInstructions_base64]):
+ self._body_file_processing(
+ save_format=self.save_format,
+ file_path=self.img_path,
+ image_key="imgInstructions",
+ captcha_link=imgInstructions_link,
+ captcha_file=imgInstructions_file,
+ captcha_base64=imgInstructions_base64,
+ **kwargs,
+ )
+ if not self.result.errorId:
+ return self._processing_response(**kwargs)
+ return self.result.to_dict()
+
+ return self._processing_response(**kwargs)
+
+ async def aio_captcha_handler(
+ self,
+ captcha_link: Optional[str] = None,
+ captcha_file: Optional[str] = None,
+ captcha_base64: Optional[bytes] = None,
+ imgInstructions_link: Optional[str] = None,
+ imgInstructions_file: Optional[str] = None,
+ imgInstructions_base64: Optional[bytes] = None,
+ **kwargs: dict[str, Any],
+ ) -> dict[str, Any]:
+ """
+ Async solving method.
+ """
+ if self.method == CoordinatesCaptchaEnm.CoordinatesTask.value:
+ await self._aio_body_file_processing(
+ save_format=self.save_format,
+ file_path=self.img_path,
+ image_key="body",
+ captcha_link=captcha_link,
+ captcha_file=captcha_file,
+ captcha_base64=captcha_base64,
+ **kwargs,
+ )
+ if any([imgInstructions_link, imgInstructions_file, imgInstructions_base64]):
+ await self._aio_body_file_processing(
+ save_format=self.save_format,
+ file_path=self.img_path,
+ image_key="imgInstructions",
+ captcha_link=imgInstructions_link,
+ captcha_file=imgInstructions_file,
+ captcha_base64=imgInstructions_base64,
+ **kwargs,
+ )
+ if not self.result.errorId:
+ return await self._aio_processing_response()
+ return self.result.to_dict()
+
+ return await self._aio_processing_response()
+
+ def __del__(self):
+ if (
+ hasattr(self, "save_format")
+ and self.save_format == SaveFormatsEnm.CONST.value
+ and hasattr(self, "img_clearing")
+ and self.img_clearing
+ ):
+ try:
+ shutil.rmtree(self.img_path)
+ except OSError:
+ pass
diff --git a/src/python_rucaptcha/yidun_captcha.py b/src/python_rucaptcha/yidun_captcha.py
new file mode 100644
index 00000000..8e773c61
--- /dev/null
+++ b/src/python_rucaptcha/yidun_captcha.py
@@ -0,0 +1,171 @@
+from typing import Union, Optional
+
+from .core.base import BaseCaptcha
+from .core.enums import YidunEnm
+
+
+class YidunCaptcha(BaseCaptcha):
+ def __init__(
+ self,
+ websiteURL: str,
+ websiteKey: str,
+ method: Union[str, YidunEnm] = YidunEnm.YidunTaskProxyless,
+ userAgent: Optional[str] = None,
+ yidunGetLib: Optional[str] = None,
+ yidunApiServerSubdomain: Optional[str] = None,
+ challenge: Optional[str] = None,
+ hcg: Optional[str] = None,
+ hct: Optional[int] = None,
+ proxyType: Optional[str] = None,
+ proxyAddress: Optional[str] = None,
+ proxyPort: Optional[int] = None,
+ proxyLogin: Optional[str] = None,
+ proxyPassword: Optional[str] = None,
+ *args,
+ **kwargs,
+ ):
+ """
+ The class is used to work with Yidun NECaptcha.
+
+ Args:
+ rucaptcha_key: User API key
+ websiteURL: Full URL of the page where the captcha is loaded
+ websiteKey: Value of the `id` (or `sitekey`) parameter from the page source
+ or from the `get?referer=` / `check?referer=` network request
+ method: Captcha type (YidunTaskProxyless or YidunTask)
+ userAgent: Browser User-Agent used to open the page
+ yidunGetLib: Full URL of the JS file that loads the captcha.
+ Recommended for Enterprise version.
+ yidunApiServerSubdomain: Yidun API server subdomain without `https://`.
+ Specify if using a custom server.
+ challenge: Dynamic challenge parameter from network requests (Enterprise)
+ hcg: Captcha hash used when forming the request (Enterprise)
+ hct: Numeric timestamp identifier for Enterprise version validation (Unix milliseconds)
+ proxyType: Proxy type (http, socks4, socks5) - required for YidunTask
+ proxyAddress: Proxy IP or hostname - required for YidunTask
+ proxyPort: Proxy port - required for YidunTask
+ proxyLogin: Proxy login (optional)
+ proxyPassword: Proxy password (optional)
+
+ Examples:
+ >>> YidunCaptcha(rucaptcha_key="aa9011f31111181111168611f1151122",
+ ... websiteURL="https://example.com/page-with-yidun",
+ ... websiteKey="0f743r3g1g...rz3grz0ym5",
+ ... method=YidunEnm.YidunTaskProxyless.value,
+ ... ).captcha_handler()
+ {
+ "errorId":0,
+ "status":"ready",
+ "solution":{
+ "token":"D19scz7n4VCU7b_...fRyEY-tXQ0cmS6laRKp_tZEyei_EUzc5M1IW0oxUHnZ4fBMH2a0jMPjOReiHVWBgkrcRYaOkQRasHlFejEToe7HZJy2jaGkxiB9b"
+ },
+ "cost":"0.003",
+ "ip":"1.2.3.4",
+ "createTime":1692863536,
+ "endTime":1692863556,
+ "solveCount":1,
+ "taskId": 73243152973,
+ }
+
+ >>> await YidunCaptcha(rucaptcha_key="aa9011f31111181111168611f1151122",
+ ... websiteURL="https://example.com/page-with-yidun",
+ ... websiteKey="0f743r3g1g...rz3grz0ym5",
+ ... method=YidunEnm.YidunTask.value,
+ ... proxyType="http",
+ ... proxyAddress="1.2.3.4",
+ ... proxyPort=8080,
+ ... ).aio_captcha_handler()
+ {
+ "errorId":0,
+ "status":"ready",
+ "solution":{
+ "token":"D19scz7n4VCU7b_...fRyEY-tXQ0cmS6laRKp_tZEyei_EUzc5M1IW0oxUHnZ4fBMH2a0jMPjOReiHVWBgkrcRYaOkQRasHlFejEToe7HZJy2jaGkxiB9b"
+ },
+ "cost":"0.003",
+ "ip":"1.2.3.4",
+ "createTime":1692863536,
+ "endTime":1692863556,
+ "solveCount":1,
+ "taskId": 73243152973,
+ }
+
+ Returns:
+ Dict with full server response
+
+ Notes:
+ https://2captcha.com/api-docs/yidun-necaptcha
+
+ https://rucaptcha.com/api-docs/yidun-necaptcha
+ """
+ super().__init__(method=method, *args, **kwargs)
+
+ # Validate method
+ if method not in YidunEnm.list_values():
+ raise ValueError(f"Invalid method parameter set, available - {YidunEnm.list_values()}")
+
+ # Build task payload
+ task_data = {
+ "websiteURL": websiteURL,
+ "websiteKey": websiteKey,
+ }
+
+ # Optional Enterprise-version and userAgent fields (only include if non-None)
+ if userAgent is not None:
+ task_data["userAgent"] = userAgent
+ if yidunGetLib is not None:
+ task_data["yidunGetLib"] = yidunGetLib
+ if yidunApiServerSubdomain is not None:
+ task_data["yidunApiServerSubdomain"] = yidunApiServerSubdomain
+ if challenge is not None:
+ task_data["challenge"] = challenge
+ if hcg is not None:
+ task_data["hcg"] = hcg
+ if hct is not None:
+ task_data["hct"] = hct
+
+ # Add proxy params only for non-proxyless methods
+ if method == YidunEnm.YidunTask.value:
+ if not all([proxyType, proxyAddress, proxyPort]):
+ raise ValueError(
+ "Proxy parameters (proxyType, proxyAddress, proxyPort) are required for YidunTask"
+ )
+ task_data.update(
+ {
+ "proxyType": proxyType,
+ "proxyAddress": proxyAddress,
+ "proxyPort": proxyPort,
+ }
+ )
+ if proxyLogin is not None:
+ task_data["proxyLogin"] = proxyLogin
+ if proxyPassword is not None:
+ task_data["proxyPassword"] = proxyPassword
+
+ self.create_task_payload["task"].update(task_data)
+
+ def captcha_handler(self, **kwargs) -> dict:
+ """
+ Sync solving method
+
+ Args:
+ kwargs: Parameters for the `requests` library
+
+ Returns:
+ Dict with full server response
+
+ Notes:
+ Check class docstring for more info
+ """
+ return self._processing_response(**kwargs)
+
+ async def aio_captcha_handler(self) -> dict:
+ """
+ Async solving method
+
+ Returns:
+ Dict with full server response
+
+ Notes:
+ Check class docstring for more info
+ """
+ return await self._aio_processing_response()
diff --git a/tests/AGENTS.md b/tests/AGENTS.md
new file mode 100644
index 00000000..83dd84dd
--- /dev/null
+++ b/tests/AGENTS.md
@@ -0,0 +1,41 @@
+# tests Package
+
+**Parent:** ./AGENTS.md
+
+## OVERVIEW
+Pytest test suite with 23+ test files covering all CAPTCHA types.
+
+## STRUCTURE
+```
+tests/
+├── __init__.py
+├── conftest.py # Fixtures + BaseTest class
+├── test_core.py # BaseCaptcha tests
+├── test_hcaptcha.py # hCaptcha tests
+├── test_recaptcha.py # reCaptcha tests
+├── test_turnstile.py # Turnstile tests
+├── ... # 20+ more test files
+└── test_image.py # Image captcha tests
+```
+
+## WHERE TO LOOK
+| Task | File |
+|------|------|
+| Add new test | Create `test_{captcha}.py` |
+| Test fixtures | `conftest.py` |
+
+## CONVENTIONS
+- **File naming**: `test_*.py` pattern
+- **Test class**: Extend `BaseTest` from `conftest.py`
+- **Run**: `make tests` or `pytest tests/`
+
+## TEST REQUIREMENTS
+- Requires `RUCAPTCHA_KEY` environment variable
+- Optional: `DEATHBYCAPTCHA_KEY` for DeathByCaptcha tests
+- Tests make real API calls to captcha services
+
+## FIXTURES (conftest.py)
+- `delay_func`: 0.5s sleep (function scope)
+- `delay_class`: 3s sleep (class scope)
+- `BaseTest`: Base test class with required env var check
+- `DeathByTest`: Subclass with optional DEATHBYCAPTCHA_KEY
diff --git a/tests/test_altcha.py b/tests/test_altcha.py
new file mode 100644
index 00000000..9fa6b94a
--- /dev/null
+++ b/tests/test_altcha.py
@@ -0,0 +1,185 @@
+import pytest
+
+from tests.conftest import BaseTest
+from python_rucaptcha.core.enums import AltchaEnm
+from python_rucaptcha.altcha_captcha import AltchaCaptcha
+from python_rucaptcha.core.serializer import GetTaskResultResponseSer
+
+
+class TestAltcha(BaseTest):
+ pageurl = "https://example.com"
+ challenge_url = "https://example.com/altcha/challenge.js"
+ challenge_json = '{"challenge":"test_data"}'
+ useragent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
+ kwargs_params = {
+ "proxyType": "socks5",
+ "proxyAddress": BaseTest.proxyAddress,
+ "proxyPort": BaseTest.proxyPort,
+ }
+
+ def test_methods_exists(self):
+ assert "captcha_handler" in AltchaCaptcha.__dict__.keys()
+ assert "aio_captcha_handler" in AltchaCaptcha.__dict__.keys()
+
+ @pytest.mark.parametrize("method", AltchaEnm.list_values())
+ def test_args(self, method: str):
+ kwargs = {}
+ if method == AltchaEnm.AltchaTask.value:
+ kwargs = {"proxyType": "http", "proxyAddress": "1.2.3.4", "proxyPort": 8080}
+ instance = AltchaCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.pageurl,
+ challengeURL=self.challenge_url,
+ userAgent=self.useragent,
+ method=method,
+ **kwargs,
+ )
+ assert instance.create_task_payload["clientKey"] == self.RUCAPTCHA_KEY
+ assert instance.create_task_payload["task"]["type"] == method
+ assert instance.create_task_payload["task"]["websiteURL"] == self.pageurl
+ assert instance.create_task_payload["task"]["challengeURL"] == self.challenge_url
+
+ def test_kwargs(self):
+ instance = AltchaCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.pageurl,
+ challengeURL=self.challenge_url,
+ method=AltchaEnm.AltchaTask,
+ **self.kwargs_params,
+ )
+ assert set(self.kwargs_params.keys()).issubset(set(instance.create_task_payload["task"].keys()))
+ assert set(self.kwargs_params.values()).issubset(set(instance.create_task_payload["task"].values()))
+
+ def test_xor_validation_both_params(self):
+ """Test that providing both challengeURL and challengeJSON raises ValueError"""
+ with pytest.raises(ValueError, match="challengeURL|challengeJSON"):
+ AltchaCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.pageurl,
+ challengeURL=self.challenge_url,
+ challengeJSON=self.challenge_json,
+ method=AltchaEnm.AltchaTaskProxyless,
+ )
+
+ def test_xor_validation_neither_param(self):
+ """Test that providing neither challengeURL nor challengeJSON raises ValueError"""
+ with pytest.raises(ValueError, match="challengeURL|challengeJSON"):
+ AltchaCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.pageurl,
+ method=AltchaEnm.AltchaTaskProxyless,
+ )
+
+ def test_valid_challenge_url(self):
+ """Test that providing only challengeURL works correctly"""
+ instance = AltchaCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.pageurl,
+ challengeURL=self.challenge_url,
+ method=AltchaEnm.AltchaTaskProxyless,
+ )
+ assert instance.create_task_payload["task"]["challengeURL"] == self.challenge_url
+ assert "challengeJSON" not in instance.create_task_payload["task"]
+
+ def test_valid_challenge_json(self):
+ """Test that providing only challengeJSON works correctly"""
+ instance = AltchaCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.pageurl,
+ challengeJSON=self.challenge_json,
+ method=AltchaEnm.AltchaTaskProxyless,
+ )
+ assert instance.create_task_payload["task"]["challengeJSON"] == self.challenge_json
+ assert "challengeURL" not in instance.create_task_payload["task"]
+
+ def test_proxy_params_in_payload(self):
+ """Test that proxy params are included in payload for AltchaTask method"""
+ instance = AltchaCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.pageurl,
+ challengeURL=self.challenge_url,
+ method=AltchaEnm.AltchaTask,
+ proxyType="http",
+ proxyAddress="1.2.3.4",
+ proxyPort=8080,
+ )
+ assert instance.create_task_payload["task"]["proxyType"] == "http"
+ assert instance.create_task_payload["task"]["proxyAddress"] == "1.2.3.4"
+ assert instance.create_task_payload["task"]["proxyPort"] == 8080
+
+ def test_wrong_method(self):
+ with pytest.raises(ValueError):
+ AltchaCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.pageurl,
+ challengeURL=self.challenge_url,
+ method=self.get_random_string(5),
+ )
+
+ """
+ Success tests
+ """
+
+ def test_basic_data(self):
+ instance = AltchaCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.pageurl,
+ challengeURL=self.challenge_url,
+ method=AltchaEnm.AltchaTaskProxyless.value,
+ )
+
+ result = instance.captcha_handler()
+
+ assert isinstance(result, dict) is True
+ if not result["errorId"]:
+ assert result["status"] == "ready"
+ assert isinstance(result["solution"], dict) is True
+ assert isinstance(result["taskId"], int) is True
+ else:
+ assert result["errorId"] in (1, 12)
+ assert result["errorCode"] == "ERROR_CAPTCHA_UNSOLVABLE"
+
+ assert result.keys() == GetTaskResultResponseSer().to_dict().keys()
+
+ async def test_aio_basic_data(self):
+ instance = AltchaCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.pageurl,
+ challengeURL=self.challenge_url,
+ method=AltchaEnm.AltchaTaskProxyless.value,
+ )
+
+ result = await instance.aio_captcha_handler()
+
+ assert isinstance(result, dict) is True
+ if not result["errorId"]:
+ assert result["status"] == "ready"
+ assert isinstance(result["solution"], dict) is True
+ assert isinstance(result["taskId"], int) is True
+ else:
+ assert result["errorId"] in (1, 12)
+ assert result["errorCode"] == "ERROR_CAPTCHA_UNSOLVABLE"
+
+ assert result.keys() == GetTaskResultResponseSer().to_dict().keys()
+
+ def test_context_basic_data(self):
+ with AltchaCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.pageurl,
+ challengeURL=self.challenge_url,
+ method=AltchaEnm.AltchaTaskProxyless.value,
+ ) as instance:
+ assert instance
+
+ async def test_context_aio_basic_data(self):
+ async with AltchaCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.pageurl,
+ challengeURL=self.challenge_url,
+ method=AltchaEnm.AltchaTaskProxyless.value,
+ ) as instance:
+ assert instance
+
+ """
+ Fail tests
+ """
diff --git a/tests/test_binance.py b/tests/test_binance.py
new file mode 100644
index 00000000..20299276
--- /dev/null
+++ b/tests/test_binance.py
@@ -0,0 +1,197 @@
+import pytest
+
+from tests.conftest import BaseTest
+from python_rucaptcha.core.enums import BinanceCaptchaEnm
+from python_rucaptcha.binance_captcha import BinanceCaptcha
+from python_rucaptcha.core.serializer import GetTaskResultResponseSer
+
+
+class TestBinanceCaptcha(BaseTest):
+ websiteURL = "https://example.com/page-with-binance"
+ websiteKey = "login"
+ validateId = "cb0bfefa598c4d2a8b65f31fde54ecd57b"
+ userAgent = (
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
+ "(KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
+ )
+ kwargs_params = {
+ "proxyType": "socks5",
+ "proxyAddress": BaseTest.proxyAddress,
+ "proxyPort": BaseTest.proxyPort,
+ }
+
+ def test_methods_exists(self):
+ assert "captcha_handler" in BinanceCaptcha.__dict__.keys()
+ assert "aio_captcha_handler" in BinanceCaptcha.__dict__.keys()
+
+ @pytest.mark.parametrize("method", BinanceCaptchaEnm.list_values())
+ def test_args(self, method: str):
+ kwargs = {}
+ if method == BinanceCaptchaEnm.BinanceTask.value:
+ kwargs = {"proxyType": "http", "proxyAddress": "1.2.3.4", "proxyPort": 8080}
+ instance = BinanceCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ validateId=self.validateId,
+ userAgent=self.userAgent,
+ method=method,
+ **kwargs,
+ )
+ assert instance.create_task_payload["clientKey"] == self.RUCAPTCHA_KEY
+ assert instance.create_task_payload["task"]["type"] == method
+ assert instance.create_task_payload["task"]["websiteURL"] == self.websiteURL
+ assert instance.create_task_payload["task"]["websiteKey"] == self.websiteKey
+ assert instance.create_task_payload["task"]["validateId"] == self.validateId
+ assert instance.create_task_payload["task"]["userAgent"] == self.userAgent
+
+ def test_kwargs(self):
+ instance = BinanceCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ validateId=self.validateId,
+ method=BinanceCaptchaEnm.BinanceTask,
+ **self.kwargs_params,
+ )
+ assert set(self.kwargs_params.keys()).issubset(set(instance.create_task_payload["task"].keys()))
+ assert set(self.kwargs_params.values()).issubset(set(instance.create_task_payload["task"].values()))
+
+ def test_proxy_params_in_payload(self):
+ instance = BinanceCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ validateId=self.validateId,
+ method=BinanceCaptchaEnm.BinanceTask,
+ proxyType="http",
+ proxyAddress="1.2.3.4",
+ proxyPort=8080,
+ )
+ assert instance.create_task_payload["task"]["proxyType"] == "http"
+ assert instance.create_task_payload["task"]["proxyAddress"] == "1.2.3.4"
+ assert instance.create_task_payload["task"]["proxyPort"] == 8080
+
+ def test_no_useragent(self):
+ instance = BinanceCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ validateId=self.validateId,
+ method=BinanceCaptchaEnm.BinanceTaskProxyless,
+ )
+ assert "userAgent" not in instance.create_task_payload["task"]
+
+ def test_missing_proxy_for_proxy_method(self):
+ with pytest.raises(ValueError, match="proxyType|proxyAddress|proxyPort"):
+ BinanceCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ validateId=self.validateId,
+ method=BinanceCaptchaEnm.BinanceTask,
+ )
+
+ def test_wrong_method(self):
+ with pytest.raises(ValueError):
+ BinanceCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ validateId=self.validateId,
+ method=self.get_random_string(5),
+ )
+
+ """
+ Success tests
+ """
+
+ def test_basic_data(self):
+ instance = BinanceCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ validateId=self.validateId,
+ method=BinanceCaptchaEnm.BinanceTaskProxyless.value,
+ )
+
+ result = instance.captcha_handler()
+
+ assert isinstance(result, dict) is True
+ if not result["errorId"]:
+ assert result["status"] in ("ready", "processing")
+ assert isinstance(result["taskId"], int) is True
+ else:
+ assert result["errorId"] in (1, 12)
+ assert result["errorCode"] == "ERROR_CAPTCHA_UNSOLVABLE"
+
+ assert result.keys() == GetTaskResultResponseSer().to_dict().keys()
+
+ async def test_aio_basic_data(self):
+ instance = BinanceCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ validateId=self.validateId,
+ method=BinanceCaptchaEnm.BinanceTaskProxyless.value,
+ )
+
+ result = await instance.aio_captcha_handler()
+
+ assert isinstance(result, dict) is True
+ if not result["errorId"]:
+ assert result["status"] in ("ready", "processing")
+ assert isinstance(result["taskId"], int) is True
+ else:
+ assert result["errorId"] in (1, 12)
+ assert result["errorCode"] in ("ERROR_CAPTCHA_UNSOLVABLE", BinanceCaptcha.NO_CAPTCHA_ERR)
+
+ assert result.keys() == GetTaskResultResponseSer().to_dict().keys()
+
+ def test_context_basic_data(self):
+ with BinanceCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ validateId=self.validateId,
+ method=BinanceCaptchaEnm.BinanceTaskProxyless.value,
+ ) as instance:
+ assert instance
+
+ async def test_context_aio_basic_data(self):
+ async with BinanceCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ validateId=self.validateId,
+ method=BinanceCaptchaEnm.BinanceTaskProxyless.value,
+ ) as instance:
+ assert instance
+
+ """
+ Fail tests
+ """
+
+ def test_no_websiteURL(self):
+ with pytest.raises(TypeError):
+ BinanceCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteKey=self.websiteKey,
+ validateId=self.validateId,
+ )
+
+ def test_no_websiteKey(self):
+ with pytest.raises(TypeError):
+ BinanceCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ validateId=self.validateId,
+ )
+
+ def test_no_validateId(self):
+ with pytest.raises(TypeError):
+ BinanceCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ )
diff --git a/tests/test_vk_captcha.py b/tests/test_vk_captcha.py
new file mode 100644
index 00000000..e3022e18
--- /dev/null
+++ b/tests/test_vk_captcha.py
@@ -0,0 +1,175 @@
+import pytest
+
+from tests.conftest import BaseTest
+from python_rucaptcha.core.enums import VKCaptchaEnm
+from python_rucaptcha.vk_captcha import VKCaptcha
+from python_rucaptcha.core.serializer import GetTaskResultResponseSer
+
+
+class TestVKCaptcha(BaseTest):
+ redirectUri = "https://id.vk.com/not_robot_captcha?domain=vk.com"
+ userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
+ captcha_link = "https://vk.com/captcha.php?sid=123456"
+
+ def test_methods_exists(self):
+ assert "captcha_handler" in VKCaptcha.__dict__.keys()
+ assert "aio_captcha_handler" in VKCaptcha.__dict__.keys()
+
+ @pytest.mark.parametrize("method", VKCaptchaEnm.list_values())
+ def test_args(self, method: str):
+ instance = VKCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ method=method,
+ redirectUri=self.redirectUri,
+ userAgent=self.userAgent,
+ proxyType="socks5",
+ proxyAddress=self.proxyAddress,
+ proxyPort=self.proxyPort,
+ )
+ assert instance.create_task_payload["clientKey"] == self.RUCAPTCHA_KEY
+ assert instance.create_task_payload["task"]["type"] == method
+
+ def test_vkcaptcha_task_payload(self):
+ instance = VKCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ redirectUri=self.redirectUri,
+ userAgent=self.userAgent,
+ proxyType="socks5",
+ proxyAddress=self.proxyAddress,
+ proxyPort=self.proxyPort,
+ )
+ task = instance.create_task_payload["task"]
+ assert task["type"] == "VKCaptchaTask"
+ assert task["redirectUri"] == self.redirectUri
+ assert task["userAgent"] == self.userAgent
+ assert task["proxyType"] == "socks5"
+ assert task["proxyAddress"] == self.proxyAddress
+ assert task["proxyPort"] == self.proxyPort
+
+ def test_vkcaptcha_task_with_proxy_auth(self):
+ instance = VKCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ redirectUri=self.redirectUri,
+ userAgent=self.userAgent,
+ proxyType="socks5",
+ proxyAddress=self.proxyAddress,
+ proxyPort=self.proxyPort,
+ proxyLogin="user1",
+ proxyPassword="pass1",
+ )
+ task = instance.create_task_payload["task"]
+ assert task["proxyLogin"] == "user1"
+ assert task["proxyPassword"] == "pass1"
+
+ def test_vkcaptcha_image_task_no_proxy_required(self):
+ instance = VKCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ method=VKCaptchaEnm.VKCaptchaImageTask,
+ )
+ assert instance.create_task_payload["task"]["type"] == "VKCaptchaImageTask"
+ assert "redirectUri" not in instance.create_task_payload["task"]
+
+ def test_vkcaptcha_image_task_default_method(self):
+ instance = VKCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ )
+ assert instance.create_task_payload["task"]["type"] == "VKCaptchaTask"
+
+ """
+ Fail tests
+ """
+
+ def test_wrong_method(self):
+ with pytest.raises(ValueError):
+ VKCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ method=self.get_random_string(length=5),
+ )
+
+ def test_vkcaptcha_task_missing_required_params(self):
+ with pytest.raises(ValueError):
+ VKCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ method=VKCaptchaEnm.VKCaptchaTask,
+ )
+
+ def test_vkcaptcha_task_missing_proxy(self):
+ with pytest.raises(ValueError):
+ VKCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ redirectUri=self.redirectUri,
+ userAgent=self.userAgent,
+ method=VKCaptchaEnm.VKCaptchaTask,
+ )
+
+ """
+ Success tests
+ """
+
+ def test_basic_data(self):
+ instance = VKCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ redirectUri=self.redirectUri,
+ userAgent=self.userAgent,
+ proxyType="socks5",
+ proxyAddress=self.proxyAddress,
+ proxyPort=self.proxyPort,
+ )
+
+ result = instance.captcha_handler()
+
+ assert isinstance(result, dict) is True
+ if not result["errorId"]:
+ assert result["status"] == "ready"
+ assert isinstance(result["solution"], dict) is True
+ assert isinstance(result["taskId"], int) is True
+ else:
+ assert result["errorId"] in (1, 12)
+ assert result["errorCode"] == "ERROR_CAPTCHA_UNSOLVABLE"
+
+ assert result.keys() == GetTaskResultResponseSer().to_dict().keys()
+
+ async def test_aio_basic_data(self):
+ instance = VKCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ redirectUri=self.redirectUri,
+ userAgent=self.userAgent,
+ proxyType="socks5",
+ proxyAddress=self.proxyAddress,
+ proxyPort=self.proxyPort,
+ )
+
+ result = await instance.aio_captcha_handler()
+
+ assert isinstance(result, dict) is True
+ if not result["errorId"]:
+ assert result["status"] == "ready"
+ assert isinstance(result["solution"], dict) is True
+ assert isinstance(result["taskId"], int) is True
+ else:
+ assert result["errorId"] in (1, 12)
+ assert result["errorCode"] == "ERROR_CAPTCHA_UNSOLVABLE"
+
+ assert result.keys() == GetTaskResultResponseSer().to_dict().keys()
+
+ def test_context_basic_data(self):
+ with VKCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ redirectUri=self.redirectUri,
+ userAgent=self.userAgent,
+ proxyType="socks5",
+ proxyAddress=self.proxyAddress,
+ proxyPort=self.proxyPort,
+ ) as instance:
+ assert instance
+
+ async def test_context_aio_basic_data(self):
+ async with VKCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ redirectUri=self.redirectUri,
+ userAgent=self.userAgent,
+ proxyType="socks5",
+ proxyAddress=self.proxyAddress,
+ proxyPort=self.proxyPort,
+ ) as instance:
+ assert instance
diff --git a/tests/test_yandex_smart_captcha.py b/tests/test_yandex_smart_captcha.py
new file mode 100644
index 00000000..412014c3
--- /dev/null
+++ b/tests/test_yandex_smart_captcha.py
@@ -0,0 +1,236 @@
+import pytest
+
+from tests.conftest import BaseTest
+from python_rucaptcha.core.enums import YandexSmartCaptchaEnm, CoordinatesCaptchaEnm, SaveFormatsEnm
+from python_rucaptcha.core.serializer import GetTaskResultResponseSer
+from python_rucaptcha.yandex_smart_captcha import YandexSmartCaptcha
+
+
+class TestYandexSmartCaptcha(BaseTest):
+ websiteURL = "https://example.com/"
+ websiteKey = "Y5Lh0ti..."
+ userAgent = (
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
+ "(KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
+ )
+ comment = "select objects in the order of the instruction"
+ captcha_file = "src/examples/088636.png"
+ instruction_file = "src/examples/bounding_box_start.png"
+ kwargs_params = {
+ "proxyType": "http",
+ "proxyAddress": "1.2.3.4",
+ "proxyPort": 8080,
+ }
+
+ def test_methods_exists(self):
+ assert "captcha_handler" in YandexSmartCaptcha.__dict__.keys()
+ assert "aio_captcha_handler" in YandexSmartCaptcha.__dict__.keys()
+
+ @pytest.mark.parametrize("method", YandexSmartCaptchaEnm.list_values() + [CoordinatesCaptchaEnm.CoordinatesTask.value])
+ def test_args(self, method: str):
+ kwargs = {}
+ if method == YandexSmartCaptchaEnm.YandexSmartCaptchaTask.value:
+ kwargs = {"proxyType": "http", "proxyAddress": "1.2.3.4", "proxyPort": 8080}
+ elif method == CoordinatesCaptchaEnm.CoordinatesTask.value:
+ kwargs = {"imgType": "smart_captcha", "comment": self.comment}
+
+ instance = YandexSmartCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ method=method,
+ **kwargs,
+ )
+ assert instance.create_task_payload["clientKey"] == self.RUCAPTCHA_KEY
+ assert instance.create_task_payload["task"]["type"] == method
+
+ def test_kwargs(self):
+ instance = YandexSmartCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ method=YandexSmartCaptchaEnm.YandexSmartCaptchaTask,
+ **self.kwargs_params,
+ )
+ assert set(self.kwargs_params.keys()).issubset(set(instance.create_task_payload["task"].keys()))
+ assert set(self.kwargs_params.values()).issubset(set(instance.create_task_payload["task"].values()))
+
+ def test_no_useragent(self):
+ instance = YandexSmartCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ method=YandexSmartCaptchaEnm.YandexSmartCaptchaTaskProxyless,
+ )
+ assert "userAgent" not in instance.create_task_payload["task"]
+
+ def test_proxy_params_in_payload(self):
+ instance = YandexSmartCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ method=YandexSmartCaptchaEnm.YandexSmartCaptchaTask,
+ proxyType="http",
+ proxyAddress="1.2.3.4",
+ proxyPort=8080,
+ )
+ assert instance.create_task_payload["task"]["proxyType"] == "http"
+ assert instance.create_task_payload["task"]["proxyAddress"] == "1.2.3.4"
+ assert instance.create_task_payload["task"]["proxyPort"] == 8080
+
+ def test_missing_proxy_for_proxy_method(self):
+ with pytest.raises(ValueError, match="proxyType|proxyAddress|proxyPort"):
+ YandexSmartCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ method=YandexSmartCaptchaEnm.YandexSmartCaptchaTask,
+ )
+
+ def test_missing_required_token_fields(self):
+ with pytest.raises(ValueError, match="websiteURL and websiteKey"):
+ YandexSmartCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ method=YandexSmartCaptchaEnm.YandexSmartCaptchaTaskProxyless,
+ )
+
+ def test_wrong_method(self):
+ with pytest.raises(ValueError):
+ YandexSmartCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ method=self.get_random_string(5),
+ )
+
+ def test_smart_captcha_missing_comment(self):
+ with pytest.raises(ValueError, match="comment"):
+ YandexSmartCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ method=CoordinatesCaptchaEnm.CoordinatesTask,
+ imgType="smart_captcha",
+ )
+
+ def test_smart_captcha_with_instructions(self):
+ instance = YandexSmartCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ method=CoordinatesCaptchaEnm.CoordinatesTask,
+ imgType="smart_captcha",
+ comment=self.comment,
+ )
+ result = instance.captcha_handler(
+ captcha_file=self.captcha_file,
+ imgInstructions_file=self.instruction_file,
+ )
+ assert isinstance(result, dict) is True
+ # The handler will attempt to call the API
+ # We just verify it constructs without exception and returns a dict
+
+ def test_pazl_smart_captcha_minimal(self):
+ instance = YandexSmartCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ method=CoordinatesCaptchaEnm.CoordinatesTask,
+ imgType="pazl_smart_captcha",
+ )
+ assert instance.create_task_payload["task"]["imgType"] == "pazl_smart_captcha"
+ assert "comment" not in instance.create_task_payload["task"]
+
+ """
+ Success tests
+ """
+
+ def test_basic_data(self):
+ instance = YandexSmartCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ method=YandexSmartCaptchaEnm.YandexSmartCaptchaTaskProxyless.value,
+ )
+
+ result = instance.captcha_handler()
+
+ assert isinstance(result, dict) is True
+ if not result["errorId"]:
+ assert result["status"] in ("ready", "processing")
+ assert isinstance(result["taskId"], int) is True
+ else:
+ assert result["errorId"] in (1, 12)
+ assert result["errorCode"] == "ERROR_CAPTCHA_UNSOLVABLE"
+
+ assert result.keys() == GetTaskResultResponseSer().to_dict().keys()
+
+ async def test_aio_basic_data(self):
+ instance = YandexSmartCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ method=YandexSmartCaptchaEnm.YandexSmartCaptchaTaskProxyless.value,
+ )
+
+ result = await instance.aio_captcha_handler()
+
+ assert isinstance(result, dict) is True
+ if not result["errorId"]:
+ assert result["status"] in ("ready", "processing")
+ assert isinstance(result["taskId"], int) is True
+ else:
+ assert result["errorId"] in (1, 12)
+ assert result["errorCode"] in ("ERROR_CAPTCHA_UNSOLVABLE", YandexSmartCaptcha.NO_CAPTCHA_ERR)
+
+ assert result.keys() == GetTaskResultResponseSer().to_dict().keys()
+
+ def test_context_basic_data(self):
+ with YandexSmartCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ method=YandexSmartCaptchaEnm.YandexSmartCaptchaTaskProxyless.value,
+ ) as instance:
+ assert instance
+
+ async def test_context_aio_basic_data(self):
+ async with YandexSmartCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ method=YandexSmartCaptchaEnm.YandexSmartCaptchaTaskProxyless.value,
+ ) as instance:
+ assert instance
+
+ def test_image_with_url(self):
+ instance = YandexSmartCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ method=CoordinatesCaptchaEnm.CoordinatesTask,
+ imgType="smart_captcha",
+ comment=self.comment,
+ )
+ result = instance.captcha_handler(
+ captcha_link=self.captcha_file,
+ imgInstructions_link=self.instruction_file,
+ )
+ assert isinstance(result, dict) is True
+
+ def test_no_captcha(self):
+ instance = YandexSmartCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ method=CoordinatesCaptchaEnm.CoordinatesTask,
+ imgType="pazl_smart_captcha",
+ )
+ result = instance.captcha_handler()
+ assert isinstance(result, dict) is True
+ assert result["errorId"] == 12
+ assert isinstance(result["errorCode"], str) is True
+ assert result.keys() == GetTaskResultResponseSer().to_dict().keys()
+
+ async def test_aio_no_captcha(self):
+ instance = YandexSmartCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ method=CoordinatesCaptchaEnm.CoordinatesTask,
+ imgType="pazl_smart_captcha",
+ )
+ result = await instance.aio_captcha_handler()
+ assert isinstance(result, dict) is True
+ assert result["errorId"] == 12
+ assert isinstance(result["errorCode"], str) is True
+ assert result.keys() == GetTaskResultResponseSer().to_dict().keys()
diff --git a/tests/test_yidun_captcha.py b/tests/test_yidun_captcha.py
new file mode 100644
index 00000000..6b367cc2
--- /dev/null
+++ b/tests/test_yidun_captcha.py
@@ -0,0 +1,192 @@
+import pytest
+
+from tests.conftest import BaseTest
+from python_rucaptcha.core.enums import YidunEnm
+from python_rucaptcha.yidun_captcha import YidunCaptcha
+from python_rucaptcha.core.serializer import GetTaskResultResponseSer
+
+
+class TestYidunCaptcha(BaseTest):
+ websiteURL = "https://example.com/page-with-yidun"
+ websiteKey = "0f743r3g1grz3grz0ym5"
+ userAgent = (
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
+ "(KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
+ )
+ kwargs_params = {
+ "proxyType": "socks5",
+ "proxyAddress": BaseTest.proxyAddress,
+ "proxyPort": BaseTest.proxyPort,
+ }
+
+ def test_methods_exists(self):
+ assert "captcha_handler" in YidunCaptcha.__dict__.keys()
+ assert "aio_captcha_handler" in YidunCaptcha.__dict__.keys()
+
+ @pytest.mark.parametrize("method", YidunEnm.list_values())
+ def test_args(self, method: str):
+ kwargs = {}
+ if method == YidunEnm.YidunTask.value:
+ kwargs = {"proxyType": "http", "proxyAddress": "1.2.3.4", "proxyPort": 8080}
+ instance = YidunCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ userAgent=self.userAgent,
+ method=method,
+ **kwargs,
+ )
+ assert instance.create_task_payload["clientKey"] == self.RUCAPTCHA_KEY
+ assert instance.create_task_payload["task"]["type"] == method
+ assert instance.create_task_payload["task"]["websiteURL"] == self.websiteURL
+ assert instance.create_task_payload["task"]["websiteKey"] == self.websiteKey
+ assert instance.create_task_payload["task"]["userAgent"] == self.userAgent
+
+ def test_kwargs(self):
+ instance = YidunCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ method=YidunEnm.YidunTask,
+ **self.kwargs_params,
+ )
+ assert set(self.kwargs_params.keys()).issubset(set(instance.create_task_payload["task"].keys()))
+ assert set(self.kwargs_params.values()).issubset(set(instance.create_task_payload["task"].values()))
+
+ def test_enterprise_params(self):
+ enterprise_params = {
+ "yidunGetLib": "https://example.com/yidun/load.min.js",
+ "yidunApiServerSubdomain": "c.dun.163.com",
+ "challenge": "0c59ba0d1b2349f9b2c1a2b3c4d5e6f7",
+ "hcg": "2c78a7731e2345f6a7b8c9d0e1f2a3b4",
+ "hct": 1779358333191,
+ }
+ instance = YidunCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ **enterprise_params,
+ )
+ for key, value in enterprise_params.items():
+ assert instance.create_task_payload["task"][key] == value
+
+ def test_no_useragent(self):
+ instance = YidunCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ method=YidunEnm.YidunTaskProxyless,
+ )
+ assert "userAgent" not in instance.create_task_payload["task"]
+
+ def test_proxy_params_in_payload(self):
+ instance = YidunCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ method=YidunEnm.YidunTask,
+ proxyType="http",
+ proxyAddress="1.2.3.4",
+ proxyPort=8080,
+ )
+ assert instance.create_task_payload["task"]["proxyType"] == "http"
+ assert instance.create_task_payload["task"]["proxyAddress"] == "1.2.3.4"
+ assert instance.create_task_payload["task"]["proxyPort"] == 8080
+
+ def test_missing_proxy_for_proxy_method(self):
+ with pytest.raises(ValueError, match="proxyType|proxyAddress|proxyPort"):
+ YidunCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ method=YidunEnm.YidunTask,
+ )
+
+ def test_wrong_method(self):
+ with pytest.raises(ValueError):
+ YidunCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ method=self.get_random_string(5),
+ )
+
+ """
+ Success tests
+ """
+
+ def test_basic_data(self):
+ instance = YidunCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ method=YidunEnm.YidunTaskProxyless.value,
+ )
+
+ result = instance.captcha_handler()
+
+ assert isinstance(result, dict) is True
+ if not result["errorId"]:
+ assert result["status"] in ("ready", "processing")
+ assert isinstance(result["taskId"], int) is True
+ else:
+ assert result["errorId"] in (1, 12)
+ assert result["errorCode"] == "ERROR_CAPTCHA_UNSOLVABLE"
+
+ assert result.keys() == GetTaskResultResponseSer().to_dict().keys()
+
+ async def test_aio_basic_data(self):
+ instance = YidunCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ method=YidunEnm.YidunTaskProxyless.value,
+ )
+
+ result = await instance.aio_captcha_handler()
+
+ assert isinstance(result, dict) is True
+ if not result["errorId"]:
+ assert result["status"] in ("ready", "processing")
+ assert isinstance(result["taskId"], int) is True
+ else:
+ assert result["errorId"] in (1, 12)
+ assert result["errorCode"] in ("ERROR_CAPTCHA_UNSOLVABLE", YidunCaptcha.NO_CAPTCHA_ERR)
+
+ assert result.keys() == GetTaskResultResponseSer().to_dict().keys()
+
+ def test_context_basic_data(self):
+ with YidunCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ method=YidunEnm.YidunTaskProxyless.value,
+ ) as instance:
+ assert instance
+
+ async def test_context_aio_basic_data(self):
+ async with YidunCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ websiteKey=self.websiteKey,
+ method=YidunEnm.YidunTaskProxyless.value,
+ ) as instance:
+ assert instance
+
+ """
+ Fail tests
+ """
+
+ def test_no_websiteURL(self):
+ with pytest.raises(TypeError):
+ YidunCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteKey=self.websiteKey,
+ )
+
+ def test_no_websiteKey(self):
+ with pytest.raises(TypeError):
+ YidunCaptcha(
+ rucaptcha_key=self.RUCAPTCHA_KEY,
+ websiteURL=self.websiteURL,
+ )