Full Code of lorien/captcha_solver for AI

master f2847d941eb1 cached
26 files
30.5 KB
8.2k tokens
65 symbols
1 requests
Download .txt
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
Download .txt
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
Download .txt
SYMBOL INDEX (65 symbols across 10 files)

FILE: captcha_solver/backend/antigate.py
  class AntigateBackend (line 14) | class AntigateBackend(ServiceBackend):
    method __init__ (line 15) | def __init__(
    method get_submit_captcha_request_data (line 24) | def get_submit_captcha_request_data(
    method parse_submit_captcha_response (line 39) | def parse_submit_captcha_response(self, res: NetworkResponse) -> str:
    method get_check_solution_request_data (line 50) | def get_check_solution_request_data(self, captcha_id: str) -> NetworkR...
    method parse_check_solution_response (line 57) | def parse_check_solution_response(self, res: NetworkResponse) -> str:

FILE: captcha_solver/backend/base.py
  class ServiceBackend (line 9) | class ServiceBackend:
    method get_submit_captcha_request_data (line 11) | def get_submit_captcha_request_data(
    method parse_submit_captcha_response (line 17) | def parse_submit_captcha_response(self, res: NetworkResponse) -> str:
    method get_check_solution_request_data (line 21) | def get_check_solution_request_data(self, captcha_id: str) -> NetworkR...
    method parse_check_solution_response (line 25) | def parse_check_solution_response(self, res: NetworkResponse) -> str:

FILE: captcha_solver/backend/browser.py
  class BrowserBackend (line 13) | class BrowserBackend(ServiceBackend):
    method setup (line 14) | def setup(self, **_kwargs: Any) -> None:
    method get_submit_captcha_request_data (line 17) | def get_submit_captcha_request_data(
    method parse_submit_captcha_response (line 27) | def parse_submit_captcha_response(self, res: NetworkResponse) -> str:
    method get_check_solution_request_data (line 30) | def get_check_solution_request_data(self, captcha_id: str) -> NetworkR...
    method parse_check_solution_response (line 34) | def parse_check_solution_response(self, res: NetworkResponse) -> str:

FILE: captcha_solver/backend/rucaptcha.py
  class RucaptchaBackend (line 6) | class RucaptchaBackend(TwocaptchaBackend):
    method __init__ (line 7) | def __init__(

FILE: captcha_solver/backend/twocaptcha.py
  class TwocaptchaBackend (line 9) | class TwocaptchaBackend(AntigateBackend):
    method __init__ (line 10) | def __init__(
    method get_submit_captcha_request_data (line 17) | def get_submit_captcha_request_data(

FILE: captcha_solver/error.py
  class CaptchaSolverError (line 6) | class CaptchaSolverError(Exception):
  class CaptchaServiceError (line 10) | class CaptchaServiceError(CaptchaSolverError):
  class SolutionNotReady (line 14) | class SolutionNotReady(CaptchaServiceError):
  class SolutionTimeoutError (line 18) | class SolutionTimeoutError(SolutionNotReady):
  class ServiceTooBusy (line 22) | class ServiceTooBusy(CaptchaServiceError):
  class BalanceTooLow (line 26) | class BalanceTooLow(CaptchaServiceError):
  class InvalidServiceBackend (line 30) | class InvalidServiceBackend(CaptchaSolverError):

FILE: captcha_solver/network.py
  class NetworkRequest (line 13) | class NetworkRequest(TypedDict):
  class NetworkResponse (line 21) | class NetworkResponse(TypedDict):
  function request (line 27) | def request(

FILE: captcha_solver/solver.py
  class NetworkConfig (line 35) | class NetworkConfig(TypedDict):
  class InvalidBackend (line 44) | class InvalidBackend(Exception):
  class CaptchaSolver (line 48) | class CaptchaSolver:
    method __init__ (line 51) | def __init__(self, backend: str | type[ServiceBackend], **kwargs: Any)...
    method setup_network_config (line 63) | def setup_network_config(self, timeout: None | int = None) -> None:
    method get_backend_class (line 67) | def get_backend_class(
    method submit_captcha (line 76) | def submit_captcha(self, image_data: bytes, **kwargs: Any) -> str:
    method check_solution (line 86) | def check_solution(self, captcha_id: str) -> str:
    method submit_captcha_with_retry (line 102) | def submit_captcha_with_retry(
    method check_solution_with_retry (line 122) | def check_solution_with_retry(
    method solve_captcha (line 148) | def solve_captcha(

FILE: tests/test_browser.py
  class BrowserTestCase (line 10) | class BrowserTestCase(TestCase):
    method setUp (line 11) | def setUp(self):
    method tearDown (line 18) | def tearDown(self):
    method test_captcha_decoded (line 22) | def test_captcha_decoded(self):

FILE: tests/test_solver.py
  class BaseSolverTestCase (line 24) | class BaseSolverTestCase(TestCase):
    method setUpClass (line 26) | def setUpClass(cls):
    method tearDownClass (line 31) | def tearDownClass(cls):
    method setUp (line 34) | def setUp(self):
  class AntigateTestCase (line 38) | class AntigateTestCase(BaseSolverTestCase):
    method setUp (line 39) | def setUp(self):
    method create_solver (line 43) | def create_solver(self, **kwargs):
    method test_post_data (line 51) | def test_post_data(self):
    method test_antigate_decoded (line 58) | def test_antigate_decoded(self):
    method test_antigate_no_slot_available (line 63) | def test_antigate_no_slot_available(self):
    method test_antigate_zero_balance (line 68) | def test_antigate_zero_balance(self):
    method test_antigate_unknown_error (line 72) | def test_antigate_unknown_error(self):
    method test_antigate_unknown_code (line 78) | def test_antigate_unknown_code(self):
    method test_solution_timeout_error (line 84) | def test_solution_timeout_error(self):
    method test_solution_unknown_error (line 90) | def test_solution_unknown_error(self):
    method test_solution_unknown_code (line 96) | def test_solution_unknown_code(self):
    method test_network_error_while_sending_captcha (line 102) | def test_network_error_while_sending_captcha(self):
    method test_network_error_while_receiving_solution (line 117) | def test_network_error_while_receiving_solution(self):
Condensed preview — 26 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (34K chars).
[
  {
    "path": ".bumpversion.cfg",
    "chars": 113,
    "preview": "[bumpversion]\ncurrent_version = 0.1.5\nfiles = captcha_solver/__init__.py pyproject.toml\ncommit = True\ntag = True\n"
  },
  {
    "path": ".flake8",
    "chars": 1671,
    "preview": "[flake8]\n# --- flake8\n# E121 continuation line under-indented for hanging indent\n# E125 continuation line with same inde"
  },
  {
    "path": ".github/workflows/check.yml",
    "chars": 589,
    "preview": "name: Linters\n\non: [push, pull_request]\n\njobs:\n  build:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n      "
  },
  {
    "path": ".github/workflows/mypy.yml",
    "chars": 555,
    "preview": "name: Types\n\non: [push, pull_request]\n\njobs:\n  build:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        "
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 1182,
    "preview": "name: Tests\n\non: [push, pull_request]\n\njobs:\n  test:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        o"
  },
  {
    "path": ".gitignore",
    "chars": 184,
    "preview": "*.pyc\n*.pyo\n*.swp\n*.swo\n*.orig\n\n/web/settings_local.py\npip-log.txt\n/.env\n/var/\n/dump/\n/src/\n/ioweb\n/.hg/\n/.pytest_cache/"
  },
  {
    "path": "LICENSE",
    "chars": 1084,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2022, Gregory Petukhov\n\nPermission is hereby granted, free of charge, to any person"
  },
  {
    "path": "Makefile",
    "chars": 1238,
    "preview": ".PHONY: bootstrap venv deps dirs clean pytest test release mypy pylint flake8 bandit check build coverage\n\nFILES_CHECK_M"
  },
  {
    "path": "README.md",
    "chars": 2109,
    "preview": "# Captcha Solver Documentation\n\n[![Test Status](https://github.com/lorien/captcha_solver/actions/workflows/test.yml/badg"
  },
  {
    "path": "captcha_solver/__init__.py",
    "chars": 148,
    "preview": "# pylint: disable=wildcard-import\nfrom captcha_solver.solver import CaptchaSolver\nfrom captcha_solver.error import *  # "
  },
  {
    "path": "captcha_solver/backend/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "captcha_solver/backend/antigate.py",
    "chars": 2524,
    "preview": "from __future__ import annotations\n\nfrom base64 import b64encode\nfrom typing import Any\nfrom urllib.parse import urlenco"
  },
  {
    "path": "captcha_solver/backend/base.py",
    "chars": 725,
    "preview": "from __future__ import annotations\n\nfrom abc import abstractmethod\nfrom typing import Any\n\nfrom ..network import Network"
  },
  {
    "path": "captcha_solver/backend/browser.py",
    "chars": 1253,
    "preview": "from __future__ import annotations\n\nimport os\nimport tempfile\nimport time\nimport webbrowser\nfrom typing import Any\n\nfrom"
  },
  {
    "path": "captcha_solver/backend/rucaptcha.py",
    "chars": 311,
    "preview": "from __future__ import annotations\n\nfrom .twocaptcha import TwocaptchaBackend\n\n\nclass RucaptchaBackend(TwocaptchaBackend"
  },
  {
    "path": "captcha_solver/backend/twocaptcha.py",
    "chars": 640,
    "preview": "from typing import Any\n\nfrom ..network import NetworkRequest\nfrom .antigate import AntigateBackend\n\nSOFTWARE_ID = 2373\n\n"
  },
  {
    "path": "captcha_solver/error.py",
    "chars": 568,
    "preview": "__all__ = ('CaptchaSolverError', 'CaptchaServiceError', 'SolutionNotReady',\n           'ServiceTooBusy', 'BalanceTooLow'"
  },
  {
    "path": "captcha_solver/network.py",
    "chars": 1099,
    "preview": "from __future__ import annotations\n\nimport typing\nfrom collections.abc import Mapping\nfrom urllib.error import HTTPError"
  },
  {
    "path": "captcha_solver/solver.py",
    "chars": 5582,
    "preview": "from __future__ import annotations\n\nimport logging\nimport socket\nimport time\nfrom copy import copy\nfrom pprint import pp"
  },
  {
    "path": "captcha_solver/types.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "pyproject.toml",
    "chars": 2401,
    "preview": "[project]\nname = \"captcha_solver\"\nversion = \"0.1.5\"\n\ndescription = \"Universal API to access captcha solving services.\"\nr"
  },
  {
    "path": "requirements_dev.txt",
    "chars": 569,
    "preview": "pip\nbumpversion\npytest\nbuild\ntwine\npyyaml\npymongo\nrunscript\ncoverage\npytest-cov\nmock\ntest_server\npytest-xdist\n\n# Code Qu"
  },
  {
    "path": "tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/test_browser.py",
    "chars": 803,
    "preview": "from __future__ import annotations\n\nfrom unittest import TestCase\n\nfrom mock import patch\n\nfrom captcha_solver import Ca"
  },
  {
    "path": "tests/test_solver.py",
    "chars": 5245,
    "preview": "from __future__ import annotations\n\nimport time\nfrom unittest import TestCase\n\nfrom test_server import Response, TestSer"
  },
  {
    "path": "tox.ini",
    "chars": 646,
    "preview": "[tox]\nenvlist = py3\nisolated_build = true\n\n[testenv]\nallowlist_externals =\n    make\n    echo\nskip_install = true\ndeps =\n"
  }
]

About this extraction

This page contains the full source code of the lorien/captcha_solver GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 26 files (30.5 KB), approximately 8.2k tokens, and a symbol index with 65 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!