Repository: lorien/captcha_solver Branch: master Commit: f2847d941eb1 Files: 26 Total size: 30.5 KB Directory structure: gitextract_y3t50or2/ ├── .bumpversion.cfg ├── .flake8 ├── .github/ │ └── workflows/ │ ├── check.yml │ ├── mypy.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── captcha_solver/ │ ├── __init__.py │ ├── backend/ │ │ ├── __init__.py │ │ ├── antigate.py │ │ ├── base.py │ │ ├── browser.py │ │ ├── rucaptcha.py │ │ └── twocaptcha.py │ ├── error.py │ ├── network.py │ ├── solver.py │ └── types.py ├── pyproject.toml ├── requirements_dev.txt ├── tests/ │ ├── __init__.py │ ├── test_browser.py │ └── test_solver.py └── tox.ini ================================================ FILE CONTENTS ================================================ ================================================ FILE: .bumpversion.cfg ================================================ [bumpversion] current_version = 0.1.5 files = captcha_solver/__init__.py pyproject.toml commit = True tag = True ================================================ FILE: .flake8 ================================================ [flake8] # --- flake8 # E121 continuation line under-indented for hanging indent # E125 continuation line with same indent as next logical line # E203 whitespace before ':' # E261 at least two spaces before inline comment # E265 block comment should start with '# ' # F401 'pprint.pprint' imported but unused, CHECKED BY PYLINT # F841 local variable 'suffix' is assigned to but never used, CHECKED BY PYLINT # W503 line break before binary operator # N818 exception name 'ElementNotFound' should be named with an Error suffix # F403: used; unable to detect undefined names # disabled because pylint "wildcard-import" does the same # --- flake8-commas # C812 missing trailing comma # C813 missing trailing comma in Python 3 # --- flake8-docstrings # D100 Missing docstring in public module # D101 Missing docstring in public class # D102 Missing docstring in public method # D103 Missing docstring in public function # D104 Missing docstring in public package # D105 Missing docstring in magic method # D107 Missing docstring in __init__ # D106 Missing docstring in public nested class # --- flake8-string-format # P101 format string does contain unindexed parameters # --- flake8-pie # PIE798 no-unnecessary-class: Consider using a module for namespacing instead # PIE786 Use precise exception handlers # PEA001 typing.Match is deprecated, use re.Match instead. # is not possible in py<3.9 # E203 Colons should not have any space before them. # does not work with Black formatting of "foo[(1 + 1) : ]" ignore=F401,C812,C813,D100,D101,D102,D103,D104,D106,D107,D105,P101,PIE798,PIE786,W503,N818,PEA001,E203,F403 max-line-length=88 inline-quotes = double max-complexity=10 ================================================ FILE: .github/workflows/check.yml ================================================ name: Linters on: [push, pull_request] jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest] python: ['3.7', '3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} - name: Install dependencies run: | pip install -U -r requirements_dev.txt pip install -U -e . - name: Run tests run: | make pylint && make flake8 && make bandit ================================================ FILE: .github/workflows/mypy.yml ================================================ name: Types on: [push, pull_request] jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest] python: ['3.7', '3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} - name: Install dependencies run: | pip install -U -r requirements_dev.txt pip install -U -e . - name: Run tests run: | make mypy ================================================ FILE: .github/workflows/test.yml ================================================ name: Tests on: [push, pull_request] jobs: test: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] python: ['3.7', '3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} - name: Install dependencies run: | pip install -U -r requirements_dev.txt pip install -U -e . - name: Run tests run: | make pytest coverage lcov -o .coverage.lcov - name: Coveralls Parallel uses: coverallsapp/github-action@master with: github-token: ${{ secrets.github_token }} path-to-lcov: ./.coverage.lcov flag-name: run-${{ matrix.os }}-${{ matrix.python }} parallel: true finish: needs: test runs-on: ubuntu-latest steps: - name: Coveralls Finished uses: coverallsapp/github-action@master with: github-token: ${{ secrets.github_token }} path-to-lcov: ./.coverage.lcov parallel-finished: true ================================================ FILE: .gitignore ================================================ *.pyc *.pyo *.swp *.swo *.orig /web/settings_local.py pip-log.txt /.env /var/ /dump/ /src/ /ioweb /.hg/ /.pytest_cache/ /build/ /.coverage /.tox/ *.egg-info /dist/ /build/ /.coverage ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2022, Gregory Petukhov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ .PHONY: bootstrap venv deps dirs clean pytest test release mypy pylint flake8 bandit check build coverage FILES_CHECK_MYPY = captcha_solver FILES_CHECK_ALL = $(FILES_CHECK_MYPY) tests bootstrap: venv deps dirs venv: virtualenv -p python3 .env deps: .env/bin/pip install -r requirements_dev.txt .env/bin/pip install -e . dirs: if [ ! -e var/run ]; then mkdir -p var/run; fi if [ ! -e var/log ]; then mkdir -p var/log; fi clean: find -name '*.pyc' -delete find -name '*.swp' -delete find -name '__pycache__' -delete pytest: pytest --cov captcha_solver --cov-report term-missing test: make check \ && make pytest \ && tox -e py37-check release: git push \ && git push --tags \ && make build \ && twine upload dist/* mypy: mypy --strict $(FILES_CHECK_MYPY) pylint: pylint -j0 $(FILES_CHECK_ALL) flake8: flake8 -j auto --max-cognitive-complexity=17 $(FILES_CHECK_ALL) bandit: bandit -qc pyproject.toml -r $(FILES_CHECK_ALL) check: echo "mypy" \ && make mypy \ && echo "pylint" \ && make pylint \ && echo "flake8" \ && make flake8 \ && echo "bandit" \ && make bandit build: rm -rf *.egg-info rm -rf dist/* python -m build --sdist coverage: pytest --cov captcha_solver --cov-report term-missing ================================================ FILE: README.md ================================================ # Captcha Solver Documentation [![Test Status](https://github.com/lorien/captcha_solver/actions/workflows/test.yml/badge.svg)](https://github.com/lorien/captcha_solver/actions/workflows/test.yml) [![Code Quality](https://github.com/lorien/captcha_solver/actions/workflows/check.yml/badge.svg)](https://github.com/lorien/captcha_solver/actions/workflows/test.yml) [![Type Check](https://github.com/lorien/captcha_solver/actions/workflows/mypy.yml/badge.svg)](https://github.com/lorien/captcha_solver/actions/workflows/mypy.yml) [![Test Coverage Status](https://coveralls.io/repos/github/lorien/captcha_solver/badge.svg)](https://coveralls.io/github/lorien/captcha_solver) [![Documentation Status](https://readthedocs.org/projects/captcha_solver/badge/?version=latest)](https://captcha_solver.readthedocs.org) Univeral API to work with captcha solving services. Feel free to give feedback in Telegram groups: [@grablab](https://t.me/grablab) and [@grablab\_ru](https://t.me/grablab_ru) ## Installation Run: `pip install -U captcha-solver` ## Twocaptcha Backend Example Service website is https://2captcha.com?from=3019071 ```python from captcha_solver import CaptchaSolver solver = CaptchaSolver('twocaptcha', api_key='2captcha.com API HERE') raw_data = open('captcha.png', 'rb').read() print(solver.solve_captcha(raw_data)) ``` ## Rucaptcha Backend Example Service website is https://rucaptcha.com?from=3019071 ```python from captcha_solver import CaptchaSolver solver = CaptchaSolver('rucaptcha', api_key='RUCAPTCHA_KEY') raw_data = open('captcha.png', 'rb').read() print(solver.solve_captcha(raw_data)) ``` ## Browser Backend Example ```python from captcha_solver import CaptchaSolver solver = CaptchaSolver('browser') raw_data = open('captcha.png', 'rb').read() print(solver.solve_captcha(raw_data)) ``` ## Antigate Backend Example Service website is http://getcaptchasolution.com/ijykrofoxz ```python from captcha_solver import CaptchaSolver solver = CaptchaSolver('antigate', api_key='ANTIGATE_KEY') raw_data = open('captcha.png', 'rb').read() print(solver.solve_captcha(raw_data)) ``` ================================================ FILE: captcha_solver/__init__.py ================================================ # pylint: disable=wildcard-import from captcha_solver.solver import CaptchaSolver from captcha_solver.error import * # noqa __version__ = '0.1.5' ================================================ FILE: captcha_solver/backend/__init__.py ================================================ ================================================ FILE: captcha_solver/backend/antigate.py ================================================ from __future__ import annotations from base64 import b64encode from typing import Any from urllib.parse import urlencode, urljoin from ..error import BalanceTooLow, CaptchaServiceError, ServiceTooBusy, SolutionNotReady from ..network import NetworkRequest, NetworkResponse from .base import ServiceBackend SOFTWARE_ID = 901 class AntigateBackend(ServiceBackend): def __init__( self, api_key: str, service_url: str = "http://antigate.com", ) -> None: super().__init__() self.api_key: None | str = api_key self.service_url: None | str = service_url def get_submit_captcha_request_data( self, data: bytes, **kwargs: Any ) -> NetworkRequest: assert self.api_key is not None post: dict[str, str | float] = { "key": self.api_key, "method": "base64", "body": b64encode(data).decode("ascii"), "soft_id": SOFTWARE_ID, } post.update(kwargs) assert self.service_url is not None url = urljoin(self.service_url, "in.php") return {"url": url, "post_data": post} def parse_submit_captcha_response(self, res: NetworkResponse) -> str: if res["code"] == 200: if res["body"].startswith(b"OK|"): return res["body"].split(b"|", 1)[1].decode("ascii") if res["body"] == b"ERROR_NO_SLOT_AVAILABLE": raise ServiceTooBusy("Service too busy") if res["body"] == b"ERROR_ZERO_BALANCE": raise BalanceTooLow("Balance too low") raise CaptchaServiceError(res["body"]) raise CaptchaServiceError("Returned HTTP code: %d" % res["code"]) def get_check_solution_request_data(self, captcha_id: str) -> NetworkRequest: assert self.api_key is not None assert self.service_url is not None params = {"key": self.api_key, "action": "get", "id": captcha_id} url = urljoin(self.service_url, "res.php?%s" % urlencode(params)) return {"url": url, "post_data": None} def parse_check_solution_response(self, res: NetworkResponse) -> str: if res["code"] == 200: if res["body"].startswith(b"OK|"): return res["body"].split(b"|", 1)[1].decode("utf8") if res["body"] == b"CAPCHA_NOT_READY": raise SolutionNotReady("Solution is not ready") raise CaptchaServiceError(res["body"]) raise CaptchaServiceError("Returned HTTP code: %d" % res["code"]) ================================================ FILE: captcha_solver/backend/base.py ================================================ from __future__ import annotations from abc import abstractmethod from typing import Any from ..network import NetworkRequest, NetworkResponse class ServiceBackend: @abstractmethod def get_submit_captcha_request_data( self, data: bytes, **kwargs: Any ) -> NetworkRequest: raise NotImplementedError @abstractmethod def parse_submit_captcha_response(self, res: NetworkResponse) -> str: raise NotImplementedError @abstractmethod def get_check_solution_request_data(self, captcha_id: str) -> NetworkRequest: raise NotImplementedError @abstractmethod def parse_check_solution_response(self, res: NetworkResponse) -> str: raise NotImplementedError ================================================ FILE: captcha_solver/backend/browser.py ================================================ from __future__ import annotations import os import tempfile import time import webbrowser from typing import Any from ..network import NetworkRequest, NetworkResponse from .base import ServiceBackend class BrowserBackend(ServiceBackend): def setup(self, **_kwargs: Any) -> None: pass def get_submit_captcha_request_data( self, data: bytes, **kwargs: Any ) -> NetworkRequest: fd, path = tempfile.mkstemp() with open(path, "wb") as out: out.write(data) os.close(fd) url = "file://" + path return {"url": url, "post_data": None} def parse_submit_captcha_response(self, res: NetworkResponse) -> str: return res["url"].replace("file://", "") def get_check_solution_request_data(self, captcha_id: str) -> NetworkRequest: url = "file://" + captcha_id return {"url": url, "post_data": None} def parse_check_solution_response(self, res: NetworkResponse) -> str: webbrowser.open(url=res["url"]) # Wait some time, skip some debug messages # which browser could dump to console time.sleep(0.5) path = res["url"].replace("file://", "") os.unlink(path) return input("Enter solution: ") ================================================ FILE: captcha_solver/backend/rucaptcha.py ================================================ from __future__ import annotations from .twocaptcha import TwocaptchaBackend class RucaptchaBackend(TwocaptchaBackend): def __init__( self, api_key: str, service_url: str = "https://rucaptcha.com", ) -> None: super().__init__(api_key=api_key, service_url=service_url) ================================================ FILE: captcha_solver/backend/twocaptcha.py ================================================ from typing import Any from ..network import NetworkRequest from .antigate import AntigateBackend SOFTWARE_ID = 2373 class TwocaptchaBackend(AntigateBackend): def __init__( self, api_key: str, service_url: str = "http://antigate.com", ) -> None: super().__init__(api_key=api_key, service_url=service_url) def get_submit_captcha_request_data( self, data: bytes, **kwargs: Any ) -> NetworkRequest: res = super().get_submit_captcha_request_data(data, **kwargs) assert res["post_data"] is not None res["post_data"]["soft_id"] = SOFTWARE_ID return res ================================================ FILE: captcha_solver/error.py ================================================ __all__ = ('CaptchaSolverError', 'CaptchaServiceError', 'SolutionNotReady', 'ServiceTooBusy', 'BalanceTooLow', 'SolutionTimeoutError', 'InvalidServiceBackend') class CaptchaSolverError(Exception): pass class CaptchaServiceError(CaptchaSolverError): pass class SolutionNotReady(CaptchaServiceError): pass class SolutionTimeoutError(SolutionNotReady): pass class ServiceTooBusy(CaptchaServiceError): pass class BalanceTooLow(CaptchaServiceError): pass class InvalidServiceBackend(CaptchaSolverError): pass ================================================ FILE: captcha_solver/network.py ================================================ from __future__ import annotations import typing from collections.abc import Mapping from urllib.error import HTTPError from urllib.parse import urlencode from urllib.request import Request, urlopen from typing_extensions import TypedDict # pylint: disable=consider-alternative-union-syntax,deprecated-typing-alias class NetworkRequest(TypedDict): url: str post_data: typing.Optional[typing.MutableMapping[str, str | float]] # pylint: enable=consider-alternative-union-syntax,deprecated-typing-alias class NetworkResponse(TypedDict): code: int body: bytes url: str def request( url: str, data: None | Mapping[str, str | float], timeout: float ) -> NetworkResponse: req_data = urlencode(data).encode("ascii") if data else None req = Request(url, req_data) try: with urlopen(req, timeout=timeout) as resp: # nosec B310 body = resp.read() code = resp.getcode() except HTTPError as ex: code = ex.code body = ex.fp.read() return { "code": code, "body": body, "url": url, } ================================================ FILE: captcha_solver/solver.py ================================================ from __future__ import annotations import logging import socket import time from copy import copy from pprint import pprint # pylint: disable=unused-import from typing import Any from urllib.error import URLError from typing_extensions import TypedDict from .backend.antigate import AntigateBackend from .backend.base import ServiceBackend from .backend.browser import BrowserBackend from .backend.rucaptcha import RucaptchaBackend from .backend.twocaptcha import TwocaptchaBackend from .error import ( InvalidServiceBackend, ServiceTooBusy, SolutionNotReady, SolutionTimeoutError, ) from .network import request LOGGER = logging.getLogger("captcha_solver") BACKEND_ALIAS: dict[str, type[ServiceBackend]] = { "2captcha": TwocaptchaBackend, "rucaptcha": RucaptchaBackend, "antigate": AntigateBackend, "browser": BrowserBackend, } class NetworkConfig(TypedDict): timeout: float DEFAULT_NETWORK_CONFIG: NetworkConfig = { "timeout": 5, } class InvalidBackend(Exception): pass class CaptchaSolver: """This class implements API to communicate with remote captcha solving service.""" def __init__(self, backend: str | type[ServiceBackend], **kwargs: Any) -> None: """Create CaptchaSolver instance. Parameters ---------- backend : string | ServiceBackend subclass Alias name of one of standard backends or class inherited from SolverBackend """ backend_cls = self.get_backend_class(backend) self.backend = backend_cls(**kwargs) self.network_config: NetworkConfig = copy(DEFAULT_NETWORK_CONFIG) def setup_network_config(self, timeout: None | int = None) -> None: if timeout is not None: self.network_config["timeout"] = timeout def get_backend_class( self, alias: str | type[ServiceBackend] ) -> type[ServiceBackend]: if isinstance(alias, str): return BACKEND_ALIAS[alias] if issubclass(alias, ServiceBackend): return alias raise InvalidServiceBackend("Invalid backend alias: %s" % alias) def submit_captcha(self, image_data: bytes, **kwargs: Any) -> str: LOGGER.debug("Submiting captcha") data = self.backend.get_submit_captcha_request_data(image_data, **kwargs) # pprint(data['post_data']) # print('URL: %s' % data['url']) response = request( data["url"], data["post_data"], timeout=self.network_config["timeout"] ) return self.backend.parse_submit_captcha_response(response) def check_solution(self, captcha_id: str) -> str: """Check if service has solved requested captcha. Raises ------ - SolutionNotReady - ServiceTooBusy """ data = self.backend.get_check_solution_request_data(captcha_id) response = request( data["url"], data["post_data"], timeout=self.network_config["timeout"], ) return self.backend.parse_check_solution_response(response) def submit_captcha_with_retry( self, submiting_time: float, submiting_delay: float, data: bytes, **kwargs: Any ) -> str: fail: None | Exception = None start_time = time.time() while True: # pylint: disable=overlapping-except try: return self.submit_captcha(image_data=data, **kwargs) except (ServiceTooBusy, URLError, socket.error, TimeoutError) as ex: fail = ex if ((time.time() + submiting_delay) - start_time) > submiting_time: break time.sleep(submiting_delay) if isinstance(fail, ServiceTooBusy): raise SolutionTimeoutError("Service has no available slots") from fail raise SolutionTimeoutError( "Could not access the service, reason: {}".format(fail) ) from fail def check_solution_with_retry( self, recognition_time: float, recognition_delay: float, captcha_id: str ) -> str: fail: None | Exception = None start_time = time.time() while True: # pylint: disable=overlapping-except try: return self.check_solution(captcha_id) except ( SolutionNotReady, socket.error, TimeoutError, ServiceTooBusy, URLError, ) as ex: fail = ex if ((time.time() + recognition_delay) - start_time) > recognition_time: break time.sleep(recognition_delay) if isinstance(fail, (ServiceTooBusy, SolutionNotReady)): raise SolutionTimeoutError( "Captcha is not ready after" " %s seconds" % recognition_time ) raise SolutionTimeoutError("Service is not available." " Error: %s" % fail) def solve_captcha( self, data: bytes, submiting_time: float = 30, submiting_delay: float = 3, recognition_time: float = 120, recognition_delay: float = 5, **kwargs: Any, ) -> str: assert submiting_time >= 0 assert submiting_delay >= 0 assert recognition_time >= 0 assert recognition_delay >= 0 captcha_id = self.submit_captcha_with_retry( submiting_time, submiting_delay, data, **kwargs ) return self.check_solution_with_retry( recognition_time, recognition_delay, captcha_id ) ================================================ FILE: captcha_solver/types.py ================================================ ================================================ FILE: pyproject.toml ================================================ [project] name = "captcha_solver" version = "0.1.5" description = "Universal API to access captcha solving services." readme = "README.md" requires-python = ">=3.7" license = {"file" = "LICENSE"} keywords = [] authors = [ {name = "Gregory Petukhov", email = "lorien@lorien.name"} ] # https://pypi.org/pypi?%3Aaction=list_classifiers classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "License :: OSI Approved :: MIT License", "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Operating System :: OS Independent", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Internet :: WWW/HTTP", "Typing :: Typed", ] dependencies = [] [project.urls] homepage = "http://github.com/lorien/captcha_solver" [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" [tool.setuptools] packages = ["captcha_solver"] [tool.setuptools.package-data] "*" = ["py.typed"] [tool.isort] profile = "black" line_length = 88 # skip_gitignore = true # throws errors in stderr when ".git" dir does not exist [tool.bandit] # B101 assert_used # B410 Using HtmlElement to parse untrusted XML data skips = ["B101", "B410"] #[[tool.mypy.overrides]] #module = "procstat" #ignore_missing_imports = true [tool.pylint.main] jobs=4 extension-pkg-whitelist="lxml" disable="missing-docstring,broad-except,too-few-public-methods,consider-using-f-string,fixme" variable-rgx="[a-z_][a-z0-9_]{1,30}$" attr-rgx="[a-z_][a-z0-9_]{1,30}$" argument-rgx="[a-z_][a-z0-9_]{1,30}$" max-line-length=88 max-args=9 load-plugins=[ "pylint.extensions.check_elif", "pylint.extensions.comparetozero", "pylint.extensions.comparison_placement", "pylint.extensions.consider_ternary_expression", "pylint.extensions.docstyle", "pylint.extensions.emptystring", "pylint.extensions.for_any_all", "pylint.extensions.overlapping_exceptions", "pylint.extensions.redefined_loop_name", "pylint.extensions.redefined_variable_type", "pylint.extensions.set_membership", "pylint.extensions.typing", ] [tool.pytest.ini_options] testpaths = ["tests"] ================================================ FILE: requirements_dev.txt ================================================ pip bumpversion pytest build twine pyyaml pymongo runscript coverage pytest-cov mock test_server pytest-xdist # Code Quality bandit[toml] flake8 # flake8-broken-line # DISABLED, DEPENCIES ISSUES flake8-bugbear # flake8-commas # DISABLED, do not like C816 missing trailing comma in Python 3.6+ flake8-comprehensions flake8-debugger flake8-docstrings flake8-expression-complexity flake8-isort flake8-pep585 flake8-pie # flake8-quotes # DISABLED, BREAKS FLAKE8 flake8-return flake8-simplify flake8-string-format flake8-cognitive-complexity mccabe mypy pep8-naming pylint ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/test_browser.py ================================================ from __future__ import annotations from unittest import TestCase from mock import patch from captcha_solver import CaptchaSolver class BrowserTestCase(TestCase): def setUp(self): self.solver = CaptchaSolver("browser") self.wb_patcher = patch("webbrowser.open") self.mock_wb_open = self.wb_patcher.start() self.raw_input_patcher = patch("captcha_solver.backend.browser.input") self.mock_raw_input = self.raw_input_patcher.start() def tearDown(self): self.wb_patcher.stop() self.raw_input_patcher.stop() def test_captcha_decoded(self): self.mock_wb_open.return_value = None self.mock_raw_input.return_value = "decoded_captcha" self.assertEqual(self.solver.solve_captcha(b"image_data"), "decoded_captcha") ================================================ FILE: tests/test_solver.py ================================================ from __future__ import annotations import time from unittest import TestCase from test_server import Response, TestServer from captcha_solver import CaptchaSolver, error # These timings means the solver will do only # one attempt to submit captcha and # one attempt to receive solution # Assuming the network timeout is greater than # submiting/recognition delays TESTING_TIME_PARAMS = { "submiting_time": 0.1, "submiting_delay": 0.2, "recognition_time": 0.1, "recognition_delay": 0.2, } TEST_SERVER_HOST = "127.0.0.1" class BaseSolverTestCase(TestCase): @classmethod def setUpClass(cls): cls.server = TestServer(address=TEST_SERVER_HOST) cls.server.start() @classmethod def tearDownClass(cls): cls.server.stop() def setUp(self): self.server.reset() class AntigateTestCase(BaseSolverTestCase): def setUp(self): super().setUp() self.solver = self.create_solver() def create_solver(self, **kwargs): config = { "service_url": self.server.get_url(), "api_key": "does not matter", } config.update(kwargs) return CaptchaSolver("antigate", **config) def test_post_data(self): data = b"foo" res = self.solver.backend.get_submit_captcha_request_data(data) body = res["post_data"]["body"] self.assertTrue(isinstance(body, str)) def test_antigate_decoded(self): self.server.add_response(Response(data=b"OK|captcha_id")) self.server.add_response(Response(data=b"OK|decoded_captcha")) self.assertEqual(self.solver.solve_captcha(b"image_data"), "decoded_captcha") def test_antigate_no_slot_available(self): self.server.add_response(Response(data=b"ERROR_NO_SLOT_AVAILABLE"), count=-1) with self.assertRaises(error.SolutionTimeoutError): self.solver.solve_captcha(b"image_data", **TESTING_TIME_PARAMS) def test_antigate_zero_balance(self): self.server.add_response(Response(data=b"ERROR_ZERO_BALANCE")) self.assertRaises(error.BalanceTooLow, self.solver.solve_captcha, b"image_data") def test_antigate_unknown_error(self): self.server.add_response(Response(data=b"UNKNOWN_ERROR")) self.assertRaises( error.CaptchaServiceError, self.solver.solve_captcha, b"image_data" ) def test_antigate_unknown_code(self): self.server.add_response(Response(status=404)) self.assertRaises( error.CaptchaServiceError, self.solver.solve_captcha, b"image_data" ) def test_solution_timeout_error(self): self.server.add_response(Response(data=b"OK|captcha_id")) self.server.add_response(Response(data=b"CAPCHA_NOT_READY")) with self.assertRaises(error.SolutionTimeoutError): self.solver.solve_captcha(b"image_data", **TESTING_TIME_PARAMS) def test_solution_unknown_error(self): self.server.add_response(Response(data=b"OK|captcha_id")) self.server.add_response(Response(data=b"UNKONWN_ERROR")) with self.assertRaises(error.CaptchaServiceError): self.solver.solve_captcha(b"image_data", **TESTING_TIME_PARAMS) def test_solution_unknown_code(self): self.server.add_response(Response(data=b"OK|captcha_id")) self.server.add_response(Response(data=b"OK|solution", status=500)) with self.assertRaises(error.CaptchaServiceError): self.solver.solve_captcha(b"image_data", **TESTING_TIME_PARAMS) def test_network_error_while_sending_captcha(self): self.server.add_response(Response(data=b"that would be timeout", sleep=0.5)) self.server.add_response(Response(data=b"OK|captcha_id")) self.server.add_response(Response(data=b"OK|solution")) solver = self.create_solver() solver.setup_network_config(timeout=0.4) solver.solve_captcha( b"image_data", submiting_time=2, submiting_delay=0, recognition_time=0, recognition_delay=0, ) def test_network_error_while_receiving_solution(self): class Callback: def __init__(self): self.step = 0 def __call__(self): self.step += 1 if self.step == 1: return { "type": "response", "data": b"OK|captcha_id", } if self.step in {2, 3, 4}: time.sleep(0.2) return { "type": "response", "data": b"that will be timeout", } return { "type": "response", "data": b"OK|solution", } solver = self.create_solver() solver.setup_network_config(timeout=0.1) self.server.add_response(Response(callback=Callback()), count=-1) solution = solver.solve_captcha( b"image_data", submiting_time=0, submiting_delay=0, recognition_time=1, recognition_delay=0.09, ) assert solution == "solution" ================================================ FILE: tox.ini ================================================ [tox] envlist = py3 isolated_build = true [testenv] allowlist_externals = make echo skip_install = true deps = -r requirements_dev.txt . [testenv:py3-test] commands = make test [testenv:py37-test] commands = make test basepython=/opt/python37/bin/python3.7 [testenv:py3-check] commands = python -V echo "pylint" make pylint echo "flake8" make flake8 echo "OK" [testenv:py37-check] commands = python -V echo "pylint" make pylint echo "flake8" make flake8 echo "OK" basepython=/opt/python37/bin/python3.7 [testenv:py37-mypy] commands = python -V make mypy basepython=/opt/python37/bin/python3.7