From 36c8b1c85180a8f259108dc3f31a39cb64c5cfed Mon Sep 17 00:00:00 2001 From: Andrei Date: Mon, 20 Mar 2023 03:58:52 +0300 Subject: [PATCH] upd structure && upd control class --- src/.coveragerc => .coveragerc | 4 +- .github/workflows/sphinx.yml | 3 - .github/workflows/test.yml | 1 - Makefile | 23 +- docs/conf.py | 6 +- docs/index.rst | 1 + docs/modules/control/example.rst | 12 + docs/modules/enum/info.rst | 6 +- docs/modules/turnstile/example.rst | 2 +- src/pyproject.toml => pyproject.toml | 0 .../AntiCaptchaAppStats.py | 63 -- src/python3_anticaptcha/AntiCaptchaControl.py | 348 ---------- src/python3_anticaptcha/control.py | 627 ++++++++++++++++++ src/python3_anticaptcha/core/base.py | 74 ++- src/python3_anticaptcha/core/enum.py | 19 +- src/python3_anticaptcha/core/serializer.py | 6 +- src/python3_anticaptcha/turnstile.py | 8 +- {src/tests => tests}/__init__.py | 0 {src/tests => tests}/conftest.py | 12 +- {src/tests => tests}/test_core.py | 2 +- 20 files changed, 751 insertions(+), 466 deletions(-) rename src/.coveragerc => .coveragerc (63%) create mode 100644 docs/modules/control/example.rst rename src/pyproject.toml => pyproject.toml (100%) delete mode 100644 src/python3_anticaptcha/AntiCaptchaAppStats.py delete mode 100644 src/python3_anticaptcha/AntiCaptchaControl.py create mode 100644 src/python3_anticaptcha/control.py rename {src/tests => tests}/__init__.py (100%) rename {src/tests => tests}/conftest.py (78%) rename {src/tests => tests}/test_core.py (97%) diff --git a/src/.coveragerc b/.coveragerc similarity index 63% rename from src/.coveragerc rename to .coveragerc index 2146cbc..39edeab 100644 --- a/src/.coveragerc +++ b/.coveragerc @@ -4,7 +4,7 @@ omit = */tests/* include = - */python3_anticaptcha/* + */src/python3_anticaptcha/* [report] @@ -13,4 +13,4 @@ omit = */tests/* include = - */python3_anticaptcha/* + */src/python3_anticaptcha/* diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index 749ed06..07662d3 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -20,9 +20,6 @@ jobs: - name: Build docs requirements run: pip install -r docs/requirements.txt - - name: Build library requirements - run: pip install -r src/requirements.txt - - name: Build docs run: make doc diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3068d4a..fd7965c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,7 +39,6 @@ jobs: run: | pip install --upgrade pip pip install -r requirements.test.txt - pip install -r src/requirements.txt - name: Test run: make tests diff --git a/Makefile b/Makefile index 52f8ae2..a70ff8f 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,7 @@ install: cd src/ && pip install -e . -tests: - cd src/ && \ +tests: install coverage run --rcfile=.coveragerc -m pytest -s tests --disable-warnings && \ coverage report --precision=3 --sort=cover --skip-empty --show-missing && \ coverage html --precision=3 --skip-empty -d coverage/html/ && \ @@ -12,31 +11,25 @@ refactor: black docs/ isort docs/ - cd src/ && \ autoflake --in-place \ --recursive \ --remove-unused-variables \ --remove-duplicate-keys \ --remove-all-unused-imports \ --ignore-init-module-imports \ - python3_anticaptcha/ tests/ && \ - black python3_anticaptcha/ tests/ && \ - isort python3_anticaptcha/ tests/ + src/ tests/ && \ + black src/ tests/ && \ + isort src/ tests/ lint: - cd src/ && \ - autoflake --in-place --recursive python3_anticaptcha/ --check && \ - black python3_anticaptcha/ --check && \ - isort python3_anticaptcha/ --check-only - -release: - pip install twine - python setup.py upload + autoflake --in-place --recursive src/ --check && \ + black src/ --check && \ + isort src/ --check-only upload: pip install twine cd src/ && python setup.py upload -doc: +doc: install cd docs/ && \ make html -e diff --git a/docs/conf.py b/docs/conf.py index a918ad2..3dd54d0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,7 +11,7 @@ sys.path.insert(0, os.path.abspath("src/")) for x in os.walk("src/python3_anticaptcha/"): sys.path.insert(0, x[0]) -from python3_anticaptcha import core, turnstile +from python3_anticaptcha import core, control, turnstile from python3_anticaptcha.__version__ import __version__ # -- Project information ----------------------------------------------------- @@ -45,9 +45,7 @@ html_show_sourcelink = False html_context = { "project_links": [ ProjectLink("PyPI Releases", "https://pypi.org/project/python3-anticaptcha/"), - ProjectLink( - "Source Code", "https://github.com/AndreiDrang/python3-anticaptcha" - ), + ProjectLink("Source Code", "https://github.com/AndreiDrang/python3-anticaptcha"), ProjectLink( "AntiCaptcha", "http://getcaptchasolution.com/vchfpctqyz", diff --git a/docs/index.rst b/docs/index.rst index 8c18480..248b1c6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -23,6 +23,7 @@ The library is intended for software developers and is used to work with the `An :caption: Captcha examples: modules/turnstile/example.rst + modules/control/example.rst .. toctree:: :maxdepth: 2 diff --git a/docs/modules/control/example.rst b/docs/modules/control/example.rst new file mode 100644 index 0000000..a937367 --- /dev/null +++ b/docs/modules/control/example.rst @@ -0,0 +1,12 @@ +Control +======= + +To import this module: + +.. code-block:: python + + from python3_anticaptcha.control import Control + + +.. autoclass:: python3_anticaptcha.control.Control + :members: \ No newline at end of file diff --git a/docs/modules/enum/info.rst b/docs/modules/enum/info.rst index ffdde0b..e51f611 100644 --- a/docs/modules/enum/info.rst +++ b/docs/modules/enum/info.rst @@ -24,6 +24,10 @@ To import this module: :members: :undoc-members: -.. autoclass:: core.enum.ProxyType +.. autoclass:: core.enum.ProxyTypeEnm + :members: + :undoc-members: + +.. autoclass:: core.enum.ControlPostfixEnm :members: :undoc-members: diff --git a/docs/modules/turnstile/example.rst b/docs/modules/turnstile/example.rst index 3485534..a4b8561 100644 --- a/docs/modules/turnstile/example.rst +++ b/docs/modules/turnstile/example.rst @@ -8,5 +8,5 @@ To import this module: from python3_anticaptcha.turnstile import Turnstile -.. autoclass:: turnstile.Turnstile +.. autoclass:: python3_anticaptcha.turnstile.Turnstile :members: \ No newline at end of file diff --git a/src/pyproject.toml b/pyproject.toml similarity index 100% rename from src/pyproject.toml rename to pyproject.toml diff --git a/src/python3_anticaptcha/AntiCaptchaAppStats.py b/src/python3_anticaptcha/AntiCaptchaAppStats.py deleted file mode 100644 index e411a38..0000000 --- a/src/python3_anticaptcha/AntiCaptchaAppStats.py +++ /dev/null @@ -1,63 +0,0 @@ -import aiohttp -import requests - -from python3_anticaptcha import get_app_stats_url - - -class AntiCaptchaAppStats: - def __init__(self, anticaptcha_key: str): - """ - Синхронный метод работы с балансом и жалобами - :param anticaptcha_key: Ключ антикапчи - """ - self.ANTICAPTCHA_KEY = anticaptcha_key - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - if exc_type: - return False - return True - - def get_stats(self, softId: int, mode: str = "errors"): - """ - Получение баланса аккаунта - :return: Возвращает актуальный баланс - """ - answer = requests.post( - get_app_stats_url, - json={"clientKey": self.ANTICAPTCHA_KEY, "softId": softId, "mode": mode}, - verify=False, - ) - - return answer.json() - - -class aioAntiCaptchaAppStats: - def __init__(self, anticaptcha_key: str): - """ - Асинхронный метод работы с балансом и жалобами - :param anticaptcha_key: Ключ антикапчи - """ - self.ANTICAPTCHA_KEY = anticaptcha_key - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - if exc_type: - return False - return True - - async def get_stats(self, softId: int, mode: str = "errors"): - """ - Получение баланса аккаунта - :return: Возвращает актуальный баланс - """ - async with aiohttp.ClientSession() as session: - async with session.post( - get_app_stats_url, - json={"clientKey": self.ANTICAPTCHA_KEY, "softId": softId, "mode": mode}, - ) as resp: - return await resp.json() diff --git a/src/python3_anticaptcha/AntiCaptchaControl.py b/src/python3_anticaptcha/AntiCaptchaControl.py deleted file mode 100644 index ab2d270..0000000 --- a/src/python3_anticaptcha/AntiCaptchaControl.py +++ /dev/null @@ -1,348 +0,0 @@ -import aiohttp -import requests - -# Адрес для получения баланса -get_balance_url = "https://api.anti-captcha.com/getBalance" -# Адрес для отправки жалобы на неверное решение капчи-изображения -incorrect_imagecaptcha_url = "https://api.anti-captcha.com/reportIncorrectImageCaptcha" -# Адрес для отправки жалобы на неверное решение ReCaptcha -incorrect_recaptcha_url = "https://api.anti-captcha.com/reportIncorrectRecaptcha" -# Адрес для получения информации о очереди -get_queue_status_url = "https://api.anti-captcha.com/getQueueStats" -# С помощью этого метода можно получить статистику трат за последние 24 часа. -get_spend_stats_url = "https://api.anti-captcha.com/getSpendingStats" -# Адрес для получения информации о приложении -get_app_stats_url = "https://api.anti-captcha.com/getAppStats" -# С помощью этого метода можно получить статистику трат за последние 24 часа. -send_funds_url = "https://api.anti-captcha.com/sendFunds" - -# available app stats mods -mods = ("errors", "views", "downloads", "users", "money") -# available complaint captcha types -complaint_types = ("image", "recaptcha") -# availalbe queue ID's -queue_ids = (1, 2, 5, 6, 7, 10, 11, 12, 13, 18, 19, 20, 21, 22) - -queues_names = ( - "English ImageToText", - "Russian ImageToText", - "Recaptcha Proxy-on", - "Recaptcha Proxyless", - "FunCaptcha", - "Funcaptcha Proxyless", - "Square Net Task", - "GeeTest Proxy-on", - "GeeTest Proxyless", -) - - -class AntiCaptchaControl: - def __init__(self, anticaptcha_key: str): - """ - Синхронный метод работы с балансом и жалобами - :param anticaptcha_key: Ключ антикапчи - """ - self.ANTICAPTCHA_KEY = anticaptcha_key - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - if exc_type: - return False - return True - - def get_balance(self) -> dict: - """ - Получение баланса аккаунта - :return: Возвращает актуальный баланс - """ - answer = requests.post(get_balance_url, json={"clientKey": self.ANTICAPTCHA_KEY}, verify=False) - - return answer.json() - - def send_funds(self, accountLogin: str = None, accountEmail: str = None, amount: float = None) -> dict: - """ - Отправить средства другому пользователю - В вашем аккаунте должна быть включена опция отправки средств через API. - Включается через службу поддержки, нужно указать причину зачем вам это требуется. - - :param accountLogin: Логин целевого аккаунта - :param accountEmail: Адрес почты целевого аккаунта - :param amount: Сумма - """ - payload = { - "clientKey": self.ANTICAPTCHA_KEY, - "accountLogin": accountLogin, - "accountEmail": accountEmail, - "amount": amount, - } - # get response - answer = requests.post(send_funds_url, json=payload, verify=False) - return answer.json() - - def get_spend_stats(self, date: int = None, queue: str = None, softId: int = None, ip: str = None) -> dict: - f""" - С помощью этого метода можно получить статистику трат за последние 24 часа. - :param date: Unix timestamp начала периода 24-х часового отчета - :param queue: Имя очереди, может быть найдено в статистике Антикапчи. - Если не указано, то возвращается суммированная статистика по всем очередям. - :param softId: ID приложения из Developers Center - :param ip: IP с которого шли запросы к API - :return: Возвращает словарь с данными трат - """ - if queue and queue not in queues_names: - raise ValueError(f"\nWrong `queue` parameter. Valid params: {queues_names}." f"\n\tYour param - `{queue}`") - payload = { - "clientKey": self.ANTICAPTCHA_KEY, - "date": date, - "queue": queue, - "softId": softId, - "ip": ip, - } - # get response - answer = requests.post(get_spend_stats_url, json=payload, verify=False) - return answer.json() - - def get_app_stats(self, softId: int, mode: str = "errors") -> dict: - """ - Получение статистики приложения - :return: Возвращает актуальный баланс - """ - if mode not in mods: - raise ValueError(f"\nWrong `mode` parameter. Valid params: {mods}." f"\n\tYour param - `{mode}`") - payload = {"clientKey": self.ANTICAPTCHA_KEY, "softId": softId, "mode": mode} - answer = requests.post(get_app_stats_url, json=payload, verify=False) - - if answer.text: - return answer.json() - else: - return {"errorId": 1} - - def complaint_on_result(self, reported_id: int, captcha_type: str = "image") -> dict: - f""" - Позволяет отправить жалобу на неправильно решённую капчу. - :param reported_id: Отправляете ID капчи на которую нужно пожаловаться - :param captcha_type: Тип капчи на который идёт жалоба. Возможные варианты: - {complaint_types} - :return: Возвращает True/False, в зависимости от результата - """ - if captcha_type not in complaint_types: - raise ValueError( - f"\nWrong `captcha_type` parameter. Valid params: {complaint_types}." - f"\n\tYour param - `{captcha_type}`" - ) - payload = {"clientKey": self.ANTICAPTCHA_KEY, "taskId": reported_id} - # complaint on image captcha - if captcha_type == "image": - answer = requests.post(incorrect_imagecaptcha_url, json=payload, verify=False) - # complaint on re-captcha - elif captcha_type == "recaptcha": - answer = requests.post(incorrect_recaptcha_url, json=payload, verify=False) - return answer.json() - - @staticmethod - def get_queue_status(queue_id: int) -> dict: - """ - Получение информации о загрузке очереди, в зависимости от ID очереди. - - Метод позволяет определить, насколько в данный момент целесообразно загружать новое задание в очередь. - Данные в выдаче кешируются на 10 секунд. - - Список ID очередей: - 1 - стандартная ImageToText, язык английский - 2 - стандартная ImageToText, язык русский - 5 - Recaptcha NoCaptcha - 6 - Recaptcha Proxyless - 7 - Funcaptcha - 10 - Funcaptcha Proxyless - 11 - Square Net Task - 12 - GeeTest Proxy-On - 13 - GeeTest Proxyless - 18 - Recaptcha V3 s0.3 - 19 - Recaptcha V3 s0.7 - 20 - Recaptcha V3 s0.9 - - Пример выдачи ответа: - { - "waiting":242, - "load":60.33, - "bid":"0.0008600982", - "speed":10.77, - "total": 610 - } - :param queue_id: Номер очереди - :return: JSON-объект - """ - - if queue_id not in queue_ids: - raise ValueError(f"\nWrong `mode` parameter. Valid params: {queue_ids}." f"\n\tYour param - `{queue_id}`") - payload = {"queueId": queue_id} - - answer = requests.post(get_queue_status_url, json=payload, verify=False) - - return answer.json() - - -class aioAntiCaptchaControl: - def __init__(self, anticaptcha_key: str): - """ - Асинхронный метод работы с балансом и жалобами - :param anticaptcha_key: Ключ антикапчи - """ - self.ANTICAPTCHA_KEY = anticaptcha_key - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - if exc_type: - return False - return True - - async def get_balance(self) -> dict: - """ - Получение баланса аккаунта - :return: Возвращает актуальный баланс - """ - async with aiohttp.ClientSession() as session: - async with session.post(get_balance_url, json={"clientKey": self.ANTICAPTCHA_KEY}) as resp: - if await resp.text(): - return await resp.json() - else: - return {"errorId": 1} - - async def send_funds(self, accountLogin: str = None, accountEmail: str = None, amount: float = None) -> dict: - """ - Отправить средства другому пользователю - В вашем аккаунте должна быть включена опция отправки средств через API. - Включается через службу поддержки, нужно указать причину зачем вам это требуется. - - :param accountLogin: Логин целевого аккаунта - :param accountEmail: Адрес почты целевого аккаунта - :param amount: Сумма - """ - payload = { - "clientKey": self.ANTICAPTCHA_KEY, - "accountLogin": accountLogin, - "accountEmail": accountEmail, - "amount": amount, - } - # get response - async with aiohttp.ClientSession() as session: - async with session.post(send_funds_url, json=payload) as resp: - if await resp.text(): - return await resp.json() - else: - return {"errorId": 1} - - async def get_spend_stats(self, date: int = None, queue: str = None, softId: int = None, ip: str = None) -> dict: - f""" - С помощью этого метода можно получить статистику трат за последние 24 часа. - :param date: Unix timestamp начала периода 24-х часового отчета - :param queue: Имя очереди, может быть найдено в статистике Антикапчи. - Если не указано, то возвращается суммированная статистика по всем очередям. - :param softId: ID приложения из Developers Center - :param ip: IP с которого шли запросы к API - :return: Возвращает словарь с данными трат - """ - if queue and queue not in queues_names: - raise ValueError(f"\nWrong `queue` parameter. Valid params: {queues_names}." f"\n\tYour param - `{queue}`") - payload = { - "clientKey": self.ANTICAPTCHA_KEY, - "date": date, - "queue": queue, - "softId": softId, - "ip": ip, - } - # get response - async with aiohttp.ClientSession() as session: - async with session.post(get_spend_stats_url, json=payload) as resp: - if await resp.text(): - return await resp.json() - else: - return {"errorId": 1} - - async def get_app_stats(self, softId: int, mode: str = "errors") -> dict: - """ - Получение баланса аккаунта - :return: Возвращает актуальный баланс - """ - if mode not in mods: - raise ValueError(f"\nWrong `mode` parameter. Valid params: {mods}." f"\n\tYour param - `{mode}`") - payload = {"clientKey": self.ANTICAPTCHA_KEY, "softId": softId, "mode": mode} - async with aiohttp.ClientSession() as session: - async with session.post(get_app_stats_url, json=payload) as resp: - if await resp.text(): - return await resp.json() - else: - return {"errorId": 1} - - async def complaint_on_result(self, reported_id: int, captcha_type: str = "image") -> dict: - f""" - Позволяет отправить жалобу на неправильно решённую капчу. - :param reported_id: Отправляете ID капчи на которую нужно пожаловаться - :param captcha_type: Тип капчи на который идёт жалоба. Возможные варианты: - {complaint_types} - :return: Возвращает True/False, в зависимости от результата - """ - if captcha_type not in complaint_types: - raise ValueError( - f"\nWrong `captcha_type` parameter. Valid params: {complaint_types}." - f"\n\tYour param - `{captcha_type}`" - ) - payload = {"clientKey": self.ANTICAPTCHA_KEY, "taskId": reported_id} - # complaint on image captcha - if captcha_type == "image": - async with aiohttp.ClientSession() as session: - async with session.post(incorrect_imagecaptcha_url, json=payload) as resp: - if await resp.text(): - return await resp.json() - else: - return {"errorId": 1} - # complaint on re-captcha - elif captcha_type == "recaptcha": - async with aiohttp.ClientSession() as session: - async with session.post(incorrect_recaptcha_url, json=payload) as resp: - if await resp.text(): - return await resp.json() - else: - return {"errorId": 1} - - @staticmethod - async def get_queue_status(queue_id: int) -> dict: - """ - Получение информации о загрузке очереди, в зависимости от ID очереди. - - Метод позволяет определить, насколько в данный момент целесообразно загружать новое задание в очередь. - Данные в выдаче кешируются на 10 секунд. - - Список ID очередей: - 1 - стандартная ImageToText, язык английский - 2 - стандартная ImageToText, язык русский - 5 - Recaptcha NoCaptcha - 6 - Recaptcha Proxyless - 7 - Funcaptcha - 10 - Funcaptcha Proxyless - - Пример выдачи ответа: - { - "waiting":242, - "load":60.33, - "bid":"0.0008600982", - "speed":10.77, - "total": 610 - } - :param queue_id: Номер очереди - :return: JSON-объект - """ - if queue_id not in queue_ids: - raise ValueError(f"\nWrong `mode` parameter. Valid params: {queue_ids}." f"\n\tYour param - `{queue_id}`") - payload = {"queueId": queue_id} - - async with aiohttp.ClientSession() as session: - async with session.post(get_queue_status_url, json=payload) as resp: - if await resp.text(): - return await resp.json() - else: - return {"errorId": 1} diff --git a/src/python3_anticaptcha/control.py b/src/python3_anticaptcha/control.py new file mode 100644 index 0000000..e9ab633 --- /dev/null +++ b/src/python3_anticaptcha/control.py @@ -0,0 +1,627 @@ +from typing import Optional + +from .core.base import BaseCaptcha +from .core.enum import CaptchaTypeEnm, ControlPostfixEnm + + +class Control(BaseCaptcha): + def __init__( + self, + api_key: str, + *args, + **kwargs, + ): + """ + The class is used to work with Turnstile. + + Args: + api_key: Capsolver API key + captcha_type: Captcha type + websiteURL: Address of the webpage + websiteKey: Turnstile sitekey + proxyType: Type of the proxy + proxyAddress: Proxy IP address IPv4/IPv6. Not allowed to use: + host names instead of IPs, + transparent proxies (where client IP is visible), + proxies from local networks (192.., 10.., 127...) + proxyPort: Proxy port. + sleep_time: The waiting time between requests to get the result of the Captcha + kwargs: Additional not required params for main request body. + Like `callbackUrl`/`languagePool` and etc. + More info - https://anti-captcha.com/apidoc/methods/createTask + + Examples: + >>> Control(api_key="99d7d111a0111dc11184111c8bb111da").get_balance() + { + "errorId": 0, + "balance": 14.12396 + } + + >>> Control.get_queue_status(queue_id=1) + { + "waiting": 234, + "load": 46.58, + "bid": 0.000576, + "speed": 8.43, + "total": 438 + } + + >>> Control(api_key="99d7d111a0111dc11184111c8bb111da").get_spending_stats(softId=867) + { + "errorId":0, + "data":[ + { + "dateFrom":1679183850, + "dateTill":1679187449, + "volume":0, + "money":0 + } + ] + } + + >>> Control(api_key="99d7d111a0111dc11184111c8bb111da").get_app_stats(softId=867, mode='views') + { + "errorId":0, + "chartData":[ + { + ...... + } + ], + "fromDate":"17 Feb 23:48", + "toDate":"19 Mar 23:48" + } + + >>> Control(api_key="99d7d111a0111dc11184111c8bb111da").report_incorrect_image(taskId=425436541) + { + "errorId":0, + "status":"success" + } + + >>> Control(api_key="99d7d111a0111dc11184111c8bb111da").report_incorrect_recaptcha(taskId=425436541) + { + "errorId":0, + "status":"success" + } + + >>> Control(api_key="99d7d111a0111dc11184111c8bb111da").report_correct_recaptcha(taskId=425436541) + { + "errorId":0, + "status":"success" + } + + >>> Control(api_key="99d7d111a0111dc11184111c8bb111da").report_incorrect_hcaptcha(taskId=425436541) + { + "errorId":0, + "status":"success" + } + + Notes: + https://anti-captcha.com/apidoc/methods/getBalance + + https://anti-captcha.com/apidoc/methods/getQueueStats + + https://anti-captcha.com/apidoc/methods/getSpendingStats + + https://anti-captcha.com/apidoc/methods/getAppStats + + https://anti-captcha.com/apidoc/methods/reportIncorrectHcaptcha + + https://anti-captcha.com/apidoc/methods/reportCorrectRecaptcha + + https://anti-captcha.com/apidoc/methods/reportIncorrectRecaptcha + + https://anti-captcha.com/apidoc/methods/reportIncorrectImageCaptcha + """ + + super().__init__(api_key=api_key, captcha_type=CaptchaTypeEnm.Control, *args, **kwargs) + + def get_balance(self) -> dict: + """ + Retrieve an account balance with its account key. + + Examples: + >>> Control(api_key="99d7d111a0111dc11184111c8bb111da").get_balance() + { + "errorId": 0, + "balance": 14.12396 + } + + Returns: + Dict with full server response + + Notes: + https://anti-captcha.com/apidoc/methods/getBalance + """ + return self._send_post_request( + session=self._session, + url_postfix=ControlPostfixEnm.GET_BALANCE, + payload={"clientKey": self._params.clientKey}, + ) + + async def aio_get_balance(self) -> dict: + """ + Async retrieve an account balance with its account key. + + Examples: + >>> await Control(api_key="99d7d111a0111dc11184111c8bb111da").aio_get_balance() + { + "errorId": 0, + "balance": 14.12396 + } + + Returns: + Dict with full server response + + Notes: + https://anti-captcha.com/apidoc/methods/getBalance + """ + + return await self._aio_send_post_request( + url_postfix=ControlPostfixEnm.GET_BALANCE, payload={"clientKey": self._params.clientKey} + ) + + @staticmethod + def get_queue_status(queue_id: int) -> dict: + """ + This method makes it possible to define a suitable time for uploading a new task + + Args: + queue_id: Identifier of a queue + + Examples: + >>> Control.get_queue_status(queue_id=1) + { + "waiting": 234, + "load": 46.58, + "bid": 0.000576, + "speed": 8.43, + "total": 438 + } + + >>> Control.get_queue_status(queue_id=20) + { + "waiting": 90, + "load": 38.36, + "bid": 0.002, + "speed": 7.38, + "total": 146 + } + + Returns: + Dict with full server response + + Notes: + https://anti-captcha.com/apidoc/methods/getQueueStats + """ + + return BaseCaptcha._send_post_request( + url_postfix=ControlPostfixEnm.GET_QUEUE_STATS, payload={"queueId": queue_id} + ) + + @staticmethod + async def aio_get_queue_status(queue_id: int) -> dict: + """ + Async method makes it possible to define a suitable time for uploading a new task + + Args: + queue_id: Identifier of a queue + + Examples: + >>> await Control.aio_get_queue_status(queue_id=1) + { + "waiting": 234, + "load": 46.58, + "bid": 0.000576, + "speed": 8.43, + "total": 438 + } + + >>> await Control.aio_get_queue_status(queue_id=20) + { + "waiting": 90, + "load": 38.36, + "bid": 0.002, + "speed": 7.38, + "total": 146 + } + + Returns: + Dict with full server response + + Notes: + https://anti-captcha.com/apidoc/methods/getQueueStats + """ + + return await BaseCaptcha._aio_send_post_request( + url_postfix=ControlPostfixEnm.GET_QUEUE_STATS, payload={"queueId": queue_id} + ) + + def get_spending_stats(self, **kwargs) -> dict: + """ + This method grabs account spendings and task volume statistics for a 24 hour period. + + Examples: + >>> Control(api_key="99d7d111a0111dc11184111c8bb111da").get_spending_stats(softId=867) + { + "errorId":0, + "data":[ + { + "dateFrom":1679183850, + "dateTill":1679187449, + "volume":0, + "money":0 + } + ] + } + + >>> Control(api_key="99d7d111a0111dc11184111c8bb111da").get_spending_stats(softId=867, + ... queue="English ImageToText") + { + "errorId":0, + "data":[ + { + "dateFrom":1679183850, + "dateTill":1679187449, + "volume":0, + "money":0 + } + ] + } + + >>> Control(api_key="99d7d111a0111dc11184111c8bb111da").get_spending_stats(queue="English ImageToText") + { + "errorId":0, + "data":[ + { + "dateFrom":1679183850, + "dateTill":1679187449, + "volume":0, + "money":0 + } + ] + } + + Returns: + Dict with full server response + + Notes: + https://anti-captcha.com/apidoc/methods/getSpendingStats + """ + return self._send_post_request( + session=self._session, + url_postfix=ControlPostfixEnm.GET_SPENDING_STATS, + payload={"clientKey": self._params.clientKey, **kwargs}, + ) + + async def aio_get_spending_stats(self, **kwargs) -> dict: + """ + Async method grabs account spendings and task volume statistics for a 24 hour period. + + Examples: + >>> await Control(api_key="99d7d111a0111dc11184111c8bb111da").aio_get_spending_stats(softId=867) + { + "errorId":0, + "data":[ + { + "dateFrom":1679183850, + "dateTill":1679187449, + "volume":0, + "money":0 + } + ] + } + + >>> await Control(api_key="99d7d111a0111dc11184111c8bb111da").aio_get_spending_stats(softId=867, + ... queue="English ImageToText") + { + "errorId":0, + "data":[ + { + "dateFrom":1679183850, + "dateTill":1679187449, + "volume":0, + "money":0 + } + ] + } + + >>> await Control(api_key="99d7d111a0111dc11184111c8bb111da").aio_get_spending_stats(queue="English ImageToText") + { + "errorId":0, + "data":[ + { + "dateFrom":1679183850, + "dateTill":1679187449, + "volume":0, + "money":0 + } + ] + } + + Returns: + Dict with full server response + + Notes: + https://anti-captcha.com/apidoc/methods/getSpendingStats + """ + return await self._aio_send_post_request( + url_postfix=ControlPostfixEnm.GET_SPENDING_STATS, payload={"clientKey": self._params.clientKey, **kwargs} + ) + + def get_app_stats(self, softId: int, mode: Optional[str] = None) -> dict: + """ + This method retrieves daily statistics for your application, which you register in Developer Center. + Statistics are available only to the application owner. Improper access returns `ERROR_ACCESS_DENIED`. + + Examples: + >>> Control(api_key="99d7d111a0111dc11184111c8bb111da").get_app_stats(softId=867, mode='views') + { + "errorId":0, + "chartData":[ + { + ...... + } + ], + "fromDate":"17 Feb 23:48", + "toDate":"19 Mar 23:48" + } + + >>> Control(api_key="99d7d111a0111dc11184111c8bb111da").get_app_stats(softId=867, mode='errors') + { + "errorId":0, + "chartData":[ + { + ...... + } + ], + "fromDate":"17 Feb 23:48", + "toDate":"19 Mar 23:48" + } + + Returns: + Dict with full server response + + Notes: + https://anti-captcha.com/apidoc/methods/getAppStats + """ + + return self._send_post_request( + session=self._session, + url_postfix=ControlPostfixEnm.GET_APP_STATS, + payload={"clientKey": self._params.clientKey, "softId": softId, "mode": mode}, + ) + + async def aio_get_app_stats(self, softId: int, mode: Optional[str] = None) -> dict: + """ + Async method retrieves daily statistics for your application, which you register in Developer Center. + Statistics are available only to the application owner. Improper access returns `ERROR_ACCESS_DENIED`. + + Examples: + >>> await Control(api_key="99d7d111a0111dc11184111c8bb111da").aio_get_app_stats(softId=867, mode='views') + { + "errorId":0, + "chartData":[ + { + ...... + } + ], + "fromDate":"17 Feb 23:48", + "toDate":"19 Mar 23:48" + } + + >>> await Control(api_key="99d7d111a0111dc11184111c8bb111da").aio_get_app_stats(softId=867, mode='errors') + { + "errorId":0, + "chartData":[ + { + ...... + } + ], + "fromDate":"17 Feb 23:48", + "toDate":"19 Mar 23:48" + } + + Returns: + Dict with full server response + + Notes: + https://anti-captcha.com/apidoc/methods/getAppStats + """ + + return await self._aio_send_post_request( + url_postfix=ControlPostfixEnm.GET_APP_STATS, + payload={"clientKey": self._params.clientKey, "softId": softId, "mode": mode}, + ) + + def report_incorrect_image(self, taskId: int) -> dict: + """ + Complaints are accepted for image captchas only. + + Examples: + >>> Control(api_key="99d7d111a0111dc11184111c8bb111da").report_incorrect_image(taskId=425436541) + { + "errorId":0, + "status":"success" + } + + + Returns: + Dict with full server response + + Notes: + https://anti-captcha.com/apidoc/methods/reportIncorrectImageCaptcha + """ + + return self._send_post_request( + session=self._session, + url_postfix=ControlPostfixEnm.REPORT_INCORRECT_IMAGE_CAPTCHA, + payload={"clientKey": self._params.clientKey, "taskId": taskId}, + ) + + async def aio_report_incorrect_image(self, taskId: int) -> dict: + """ + Async complaints are accepted for image captchas only. + + Examples: + >>> await Control(api_key="99d7d111a0111dc11184111c8bb111da").aio_report_incorrect_image(taskId=425436541) + { + "errorId":0, + "status":"success" + } + + + Returns: + Dict with full server response + + Notes: + https://anti-captcha.com/apidoc/methods/reportIncorrectImageCaptcha + """ + + return await self._aio_send_post_request( + url_postfix=ControlPostfixEnm.REPORT_INCORRECT_IMAGE_CAPTCHA, + payload={"clientKey": self._params.clientKey, "taskId": taskId}, + ) + + def report_incorrect_recaptcha(self, taskId: int) -> dict: + """ + Complaints are accepted for V2 and V3 Recaptchas only, including Enterprise Recaptcha. + + Examples: + >>> Control(api_key="99d7d111a0111dc11184111c8bb111da").report_incorrect_recaptcha(taskId=425436541) + { + "errorId":0, + "status":"success" + } + + + Returns: + Dict with full server response + + Notes: + https://anti-captcha.com/apidoc/methods/reportIncorrectRecaptcha + """ + + return self._send_post_request( + session=self._session, + url_postfix=ControlPostfixEnm.REPORT_INCORRECT_RECAPTCHA, + payload={"clientKey": self._params.clientKey, "taskId": taskId}, + ) + + async def aio_report_incorrect_recaptcha(self, taskId: int) -> dict: + """ + Async complaints are accepted for V2 and V3 Recaptchas only, including Enterprise Recaptcha. + + Examples: + >>> await Control(api_key="99d7d111a0111dc11184111c8bb111da").aio_report_incorrect_recaptcha(taskId=425436541) + { + "errorId":0, + "status":"success" + } + + + Returns: + Dict with full server response + + Notes: + https://anti-captcha.com/apidoc/methods/reportIncorrectRecaptcha + """ + return await self._aio_send_post_request( + url_postfix=ControlPostfixEnm.REPORT_INCORRECT_RECAPTCHA, + payload={"clientKey": self._params.clientKey, "taskId": taskId}, + ) + + def report_correct_recaptcha(self, taskId: int) -> dict: + """ + Reporting correctly solved ReCaptcha + + Examples: + >>> Control(api_key="99d7d111a0111dc11184111c8bb111da").report_correct_recaptcha(taskId=425436541) + { + "errorId":0, + "status":"success" + } + + + Returns: + Dict with full server response + + Notes: + https://anti-captcha.com/apidoc/methods/reportCorrectRecaptcha + """ + + return self._send_post_request( + session=self._session, + url_postfix=ControlPostfixEnm.REPORT_CORRECT_RECAPTCHA, + payload={"clientKey": self._params.clientKey, "taskId": taskId}, + ) + + async def aio_report_correct_recaptcha(self, taskId: int) -> dict: + """ + Async reporting correctly solved ReCaptcha + + Examples: + >>> await Control(api_key="99d7d111a0111dc11184111c8bb111da").aio_report_correct_recaptcha(taskId=425436541) + { + "errorId":0, + "status":"success" + } + + + Returns: + Dict with full server response + + Notes: + https://anti-captcha.com/apidoc/methods/reportCorrectRecaptcha + """ + return await self._aio_send_post_request( + url_postfix=ControlPostfixEnm.REPORT_CORRECT_RECAPTCHA, + payload={"clientKey": self._params.clientKey, "taskId": taskId}, + ) + + def report_incorrect_hcaptcha(self, taskId: int) -> dict: + """ + Use this method to send us information about tokens which did not pass on target service + + Examples: + >>> Control(api_key="99d7d111a0111dc11184111c8bb111da").report_incorrect_hcaptcha(taskId=425436541) + { + "errorId":0, + "status":"success" + } + + + Returns: + Dict with full server response + + Notes: + https://anti-captcha.com/apidoc/methods/reportIncorrectHcaptcha + """ + + return self._send_post_request( + session=self._session, + url_postfix=ControlPostfixEnm.REPORT_INCORRECT_HCAPTCHA, + payload={"clientKey": self._params.clientKey, "taskId": taskId}, + ) + + async def aio_report_incorrect_hcaptcha(self, taskId: int) -> dict: + """ + Async method to send us information about tokens which did not pass on target service + + Examples: + >>> await Control(api_key="99d7d111a0111dc11184111c8bb111da").aio_report_incorrect_hcaptcha(taskId=4256541) + { + "errorId":0, + "status":"success" + } + + + Returns: + Dict with full server response + + Notes: + https://anti-captcha.com/apidoc/methods/reportIncorrectHcaptcha + """ + return await self._aio_send_post_request( + url_postfix=ControlPostfixEnm.REPORT_INCORRECT_HCAPTCHA, + payload={"clientKey": self._params.clientKey, "taskId": taskId}, + ) diff --git a/src/python3_anticaptcha/core/base.py b/src/python3_anticaptcha/core/base.py index e7c0c40..fe4c236 100644 --- a/src/python3_anticaptcha/core/base.py +++ b/src/python3_anticaptcha/core/base.py @@ -1,7 +1,7 @@ import time import asyncio import logging -from typing import Union +from typing import Union, Optional from urllib import parse import aiohttp @@ -35,24 +35,30 @@ class BaseCaptcha: request_url: API address for sending requests """ - def __init__(self, api_key: str, captcha_type: Union[CaptchaTypeEnm, str], sleep_time: int, **kwargs): + def __init__( + self, + api_key: str, + captcha_type: Union[CaptchaTypeEnm, str], + sleep_time: int = 15, + **kwargs, + ): # validate captcha_type parameter if captcha_type not in CaptchaTypeEnm.list_values(): raise ValueError(f"Invalid `captcha_type` parameter set, available - {CaptchaTypeEnm.list_values()}") self.__sleep_time = sleep_time # assign args to validator - self.__params = CreateTaskRequestSer(clientKey=api_key, **locals()) + self._params = CreateTaskRequestSer(clientKey=api_key, **locals()) # `task` body for task creation payload self.task_params = {} # prepare `get task result` payload self._get_result_params = GetTaskResultRequestSer(clientKey=api_key) # prepare session - self.__session = requests.Session() - self.__session.mount("http://", HTTPAdapter(max_retries=RETRIES)) - self.__session.mount("https://", HTTPAdapter(max_retries=RETRIES)) - self.__session.verify = False + self._session = requests.Session() + self._session.mount("http://", HTTPAdapter(max_retries=RETRIES)) + self._session.mount("https://", HTTPAdapter(max_retries=RETRIES)) + self._session.verify = False """ Sync part @@ -60,7 +66,7 @@ class BaseCaptcha: def _processing_captcha(self) -> dict: # added task params to payload - self.__params.task = self.task_params + self._params.task = self.task_params created_task = self._create_task() @@ -69,12 +75,12 @@ class BaseCaptcha: return self._get_result().dict() return created_task.dict() - def _create_task(self) -> CreateTaskResponseSer: + def _create_task(self, url_postfix: str = CREATE_TASK_POSTFIX) -> CreateTaskResponseSer: """ Function send SYNC request to service and wait for result """ try: - resp = self.__session.post(parse.urljoin(BASE_REQUEST_URL, CREATE_TASK_POSTFIX), json=self.__params.dict()) + resp = self._session.post(parse.urljoin(BASE_REQUEST_URL, url_postfix), json=self._params.dict()) if resp.status_code == 200: return CreateTaskResponseSer(**resp.json()) else: @@ -93,7 +99,7 @@ class BaseCaptcha: attempts = attempts_generator() for _ in attempts: try: - task_result_response = self.__session.post( + task_result_response = self._session.post( parse.urljoin(BASE_REQUEST_URL, GET_RESULT_POSTFIX), json=self._get_result_params.dict() ) if task_result_response.status_code == 200: @@ -114,13 +120,32 @@ class BaseCaptcha: logging.exception(error) raise + @staticmethod + def _send_post_request( + payload: Optional[dict] = None, + session: requests.Session = requests.Session(), + url_postfix: str = CREATE_TASK_POSTFIX, + ) -> dict: + """ + Function send SYNC request to service and wait for result + """ + try: + resp = session.post(parse.urljoin(BASE_REQUEST_URL, url_postfix), json=payload) + if resp.status_code == 200: + return resp.json() + else: + raise ValueError(resp.raise_for_status()) + except Exception as error: + logging.exception(error) + raise + """ Async part """ async def _aio_processing_captcha(self) -> dict: # added task params to payload - self.__params.task = self.task_params + self._params.task = self.task_params created_task = await self._aio_create_task() @@ -130,15 +155,13 @@ class BaseCaptcha: return result.dict() return created_task.dict() - async def _aio_create_task(self) -> CreateTaskResponseSer: + async def _aio_create_task(self, url_postfix: str = CREATE_TASK_POSTFIX) -> CreateTaskResponseSer: """ Function send SYNC request to service and wait for result """ async with aiohttp.ClientSession() as session: try: - async with session.post( - parse.urljoin(BASE_REQUEST_URL, CREATE_TASK_POSTFIX), json=self.__params.dict() - ) as resp: + async with session.post(parse.urljoin(BASE_REQUEST_URL, url_postfix), json=self._params.dict()) as resp: if resp.status == 200: return CreateTaskResponseSer(**await resp.json()) else: @@ -180,6 +203,25 @@ class BaseCaptcha: logging.exception(error) raise + @staticmethod + async def _aio_send_post_request(payload: Optional[dict] = None, url_postfix: str = CREATE_TASK_POSTFIX) -> dict: + """ + Function send ASYNC request to service and wait for result + """ + + async with aiohttp.ClientSession() as session: + try: + async with session.post(parse.urljoin(BASE_REQUEST_URL, url_postfix), json=payload) as resp: + if resp.status == 200: + return await resp.json() + else: + raise ValueError(resp.reason) + except Exception as error: + logging.exception(error) + raise + + # Context methods + def __enter__(self): return self diff --git a/src/python3_anticaptcha/core/enum.py b/src/python3_anticaptcha/core/enum.py index 6590659..733b112 100644 --- a/src/python3_anticaptcha/core/enum.py +++ b/src/python3_anticaptcha/core/enum.py @@ -78,7 +78,7 @@ class ResponseStatusEnm(str, MyEnum): ready = "ready" # Task is complete; you'll find a solution in the solution property -class ProxyType(str, MyEnum): +class ProxyTypeEnm(str, MyEnum): """ Enum store proxy types """ @@ -87,3 +87,20 @@ class ProxyType(str, MyEnum): https = "https" socks4 = "socks4" socks5 = "socks5" + + +class ControlPostfixEnm(str, MyEnum): + """ + Enum store control methods URLs postfix + """ + + # get account info + GET_BALANCE = "getBalance" + GET_QUEUE_STATS = "getQueueStats" + GET_APP_STATS = "getAppStats" + GET_SPENDING_STATS = "getSpendingStats" + # reports + REPORT_INCORRECT_IMAGE_CAPTCHA = "reportIncorrectImageCaptcha" + REPORT_INCORRECT_RECAPTCHA = "reportIncorrectRecaptcha" + REPORT_CORRECT_RECAPTCHA = "reportCorrectRecaptcha" + REPORT_INCORRECT_HCAPTCHA = "reportIncorrectHcaptcha" diff --git a/src/python3_anticaptcha/core/serializer.py b/src/python3_anticaptcha/core/serializer.py index 6831309..ad190e0 100644 --- a/src/python3_anticaptcha/core/serializer.py +++ b/src/python3_anticaptcha/core/serializer.py @@ -2,7 +2,7 @@ from typing import Dict from pydantic import Field, BaseModel, constr -from python3_anticaptcha.core.enum import ProxyType, CaptchaTypeEnm, ResponseStatusEnm +from python3_anticaptcha.core.enum import ProxyTypeEnm, CaptchaTypeEnm, ResponseStatusEnm from python3_anticaptcha.core.config import APP_KEY @@ -17,7 +17,7 @@ class BaseAPIRequestSer(MyBaseModel): class BaseAPIResponseSer(MyBaseModel): - errorId: int = Field(..., description="Error identifier.") + errorId: int = Field(None, description="Error identifier.") errorCode: str = Field(None, description="An error code.") errorDescription: str = Field(None, description="Short description of the error.") @@ -34,7 +34,7 @@ class CreateTaskRequestTaskSer(MyBaseModel): class ProxyDataOptionsSer(MyBaseModel): - proxyType: ProxyType = Field(..., description="Type of proxy") + proxyType: ProxyTypeEnm = Field(..., description="Type of proxy") proxyAddress: str = Field( ..., description="Proxy IP address ipv4/ipv6. No host names or IP addresses from local networks", diff --git a/src/python3_anticaptcha/turnstile.py b/src/python3_anticaptcha/turnstile.py index 46d9019..630305e 100644 --- a/src/python3_anticaptcha/turnstile.py +++ b/src/python3_anticaptcha/turnstile.py @@ -1,8 +1,8 @@ from typing import Union, Optional -from python3_anticaptcha.core.base import BaseCaptcha -from python3_anticaptcha.core.enum import ProxyType, CaptchaTypeEnm -from python3_anticaptcha.core.serializer import TurnstileOptionsSer, TurnstileProxylessOptionsSer +from .core.base import BaseCaptcha +from .core.enum import ProxyTypeEnm, CaptchaTypeEnm +from .core.serializer import TurnstileOptionsSer, TurnstileProxylessOptionsSer class Turnstile(BaseCaptcha): @@ -12,7 +12,7 @@ class Turnstile(BaseCaptcha): captcha_type: Union[CaptchaTypeEnm, str], websiteURL: str, websiteKey: str, - proxyType: Optional[Union[ProxyType, str]] = None, + proxyType: Optional[Union[ProxyTypeEnm, str]] = None, proxyAddress: Optional[str] = None, proxyPort: Optional[int] = None, sleep_time: Optional[int] = 10, diff --git a/src/tests/__init__.py b/tests/__init__.py similarity index 100% rename from src/tests/__init__.py rename to tests/__init__.py diff --git a/src/tests/conftest.py b/tests/conftest.py similarity index 78% rename from src/tests/conftest.py rename to tests/conftest.py index e850201..99c7d43 100755 --- a/src/tests/conftest.py +++ b/tests/conftest.py @@ -7,11 +7,17 @@ import pytest @pytest.fixture(scope="function") -def delay(): - time.sleep(1) +def delay_func(): + time.sleep(5) -@pytest.mark.usefixtures("delay") +@pytest.fixture(scope="class") +def delay_class(): + time.sleep(10) + + +@pytest.mark.usefixtures("delay_func") +@pytest.mark.usefixtures("delay_class") class BaseTest: API_KEY = os.getenv("API_KEY", "ad9053f3182ca81755768608fa75") sleep_time = 5 diff --git a/src/tests/test_core.py b/tests/test_core.py similarity index 97% rename from src/tests/test_core.py rename to tests/test_core.py index 90504c9..91de227 100644 --- a/src/tests/test_core.py +++ b/tests/test_core.py @@ -1,7 +1,7 @@ from tenacity import AsyncRetrying from urllib3.util.retry import Retry -from src.tests.conftest import BaseTest +from tests.conftest import BaseTest from python3_anticaptcha.core.base import BaseCaptcha from python3_anticaptcha.core.enum import CaptchaTypeEnm from python3_anticaptcha.core.config import RETRIES, ASYNC_RETRIES, BASE_REQUEST_URL, attempts_generator