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
[](https://github.com/lorien/captcha_solver/actions/workflows/test.yml)
[](https://github.com/lorien/captcha_solver/actions/workflows/test.yml)
[](https://github.com/lorien/captcha_solver/actions/workflows/mypy.yml)
[](https://coveralls.io/github/lorien/captcha_solver)
[](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
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
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[. 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.