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