Skip to content
Merged
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ Unreleased
Added
#####

- Add ``AsyncAnticaptchaClient`` and ``AsyncJob`` for async/await usage with ``httpx`` (``pip install python-anticaptcha[async]``)
- Rename ``base.py`` → ``sync_client.py`` for symmetry with ``async_client.py``; backward-compatible ``base.py`` shim preserved
- Rename sync example files with ``sync_`` prefix to match ``async_`` examples
- Add context manager support to ``AnticaptchaClient`` (``__enter__``, ``__exit__``, ``close``)
- Add ``ANTICAPTCHA_API_KEY`` environment variable fallback for ``AnticaptchaClient``
- Add ``Proxy`` frozen dataclass with ``parse_url()`` and ``to_kwargs()`` methods
Expand Down
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ Install as standard Python package using:
pip install python-anticaptcha
```

For async support (FastAPI, aiohttp, Starlette, etc.):

```
pip install python-anticaptcha[async]
```

## Usage

To use this library you need [Anticaptcha.com](http://getcaptchasolution.com/p9bwplkicx) API key.
Expand All @@ -42,6 +48,29 @@ with AnticaptchaClient(api_key) as client:
job.join()
```

## Async Usage

For async frameworks, use `AsyncAnticaptchaClient` — the API mirrors the sync
client but all methods are awaitable:

```python
from python_anticaptcha import AsyncAnticaptchaClient, NoCaptchaTaskProxylessTask

api_key = '174faff8fbc769e94a5862391ecfd010'
site_key = '6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-'
url = 'https://www.google.com/recaptcha/api2/demo'

async with AsyncAnticaptchaClient(api_key) as client:
task = NoCaptchaTaskProxylessTask(url, site_key)
job = await client.create_task(task)
await job.join()
print(job.get_solution_response())
```

The full integration example is available in file `examples/async_recaptcha_request.py`.

## Sync Usage

### Solve recaptcha

Example snippet for Recaptcha:
Expand All @@ -60,7 +89,7 @@ job.join()
print(job.get_solution_response())
```

The full integration example is available in file `examples/recaptcha_request.py`.
The full integration example is available in file `examples/sync_recaptcha_request.py`.

If you process the same page many times, to increase reliability you can specify
whether the captcha is visible or not. This parameter is not required, as the
Expand Down
13 changes: 10 additions & 3 deletions docs/api.rst
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
API
===

Base
----
Sync Client
-----------

.. automodule:: python_anticaptcha.base
.. automodule:: python_anticaptcha.sync_client
:members:
:undoc-members:

Async Client
------------

.. automodule:: python_anticaptcha.async_client
:members:
:undoc-members:

Expand Down
31 changes: 30 additions & 1 deletion docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,35 @@ The client can be used as a context manager to ensure the underlying session is
job = client.create_task(task)
job.join()

Async client
############

.. note::

Requires the ``async`` extra: ``pip install python-anticaptcha[async]``

For async frameworks (FastAPI, aiohttp, Starlette) use ``AsyncAnticaptchaClient`` —
the API mirrors the sync client but all methods are awaitable:

.. code:: python

from python_anticaptcha import AsyncAnticaptchaClient, NoCaptchaTaskProxylessTask

api_key = '174faff8fbc769e94a5862391ecfd010'
site_key = '6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-'
url = 'https://www.google.com/recaptcha/api2/demo'

async with AsyncAnticaptchaClient(api_key) as client:
task = NoCaptchaTaskProxylessTask(url, site_key)
job = await client.create_task(task)
await job.join()
print(job.get_solution_response())

The full integration example is available in file ``examples/async_recaptcha_request.py``.

Sync client
###########

Solve recaptcha
###############

Expand All @@ -40,7 +69,7 @@ Example snippet for Recaptcha:
job.join()
print(job.get_solution_response())

The full integration example is available in file ``examples/recaptcha_request.py``.
The full integration example is available in file ``examples/sync_recaptcha_request.py``.

If you process the same page many times, to increase reliability you can specify
whether the captcha is visible or not. This parameter is not required, as the
Expand Down
17 changes: 17 additions & 0 deletions examples/async_balance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import asyncio
from os import environ
from pprint import pprint

from python_anticaptcha import AsyncAnticaptchaClient

api_key = environ["KEY"]


async def process():
async with AsyncAnticaptchaClient(api_key) as client:
balance = await client.get_balance()
pprint(balance)


if __name__ == "__main__":
asyncio.run(process())
40 changes: 40 additions & 0 deletions examples/async_recaptcha_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import asyncio
import re
from os import environ

import httpx

from python_anticaptcha import AsyncAnticaptchaClient, NoCaptchaTaskProxylessTask

api_key = environ["KEY"]
site_key_pattern = 'data-sitekey="(.+?)"'
url = "https://www.google.com/recaptcha/api2/demo?invisible=false"
EXPECTED_RESULT = "Verification Success... Hooray!"


async def get_form_html(session: httpx.AsyncClient) -> str:
return (await session.get(url)).text


async def get_token(client: AsyncAnticaptchaClient, form_html: str) -> str:
site_key = re.search(site_key_pattern, form_html).group(1)
task = NoCaptchaTaskProxylessTask(website_url=url, website_key=site_key)
job = await client.create_task(task)
await job.join()
return job.get_solution_response()


async def form_submit(session: httpx.AsyncClient, token: str) -> str:
return (await session.post(url, data={"g-recaptcha-response": token})).text


async def process():
async with AsyncAnticaptchaClient(api_key) as client, httpx.AsyncClient() as session:
html = await get_form_html(session)
token = await get_token(client, html)
return await form_submit(session, token)


if __name__ == "__main__":
result = asyncio.run(process())
assert "Verification Success... Hooray!" in result
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ license = "MIT"
requires-python = ">=3.9"
dependencies = ["requests"]
dynamic = ["version"]
keywords = ["recaptcha", "captcha", "development"]
keywords = ["recaptcha", "captcha", "development", "asyncio", "async", "httpx"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
Expand All @@ -21,14 +21,16 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Framework :: AsyncIO",
"Topic :: Internet :: WWW/HTTP",
]

[project.urls]
Homepage = "https://github.com/ad-m/python-anticaptcha"

[project.optional-dependencies]
async = ["httpx>=0.24"]
tests = ["pytest", "retry", "selenium"]
tests = ["pytest", "pytest-asyncio", "httpx>=0.24", "retry", "selenium", "six"]
docs = ["sphinx", "sphinx-rtd-theme"]

[tool.setuptools.package-data]
Expand Down
15 changes: 14 additions & 1 deletion python_anticaptcha/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import contextlib
from importlib.metadata import PackageNotFoundError, version

from .base import AnticaptchaClient, Job
from .exceptions import AnticaptchaException
from .proxy import Proxy
from .sync_client import AnticaptchaClient, Job
from .tasks import (
AntiGateTask,
AntiGateTaskProxyless,
Expand All @@ -28,6 +28,17 @@
with contextlib.suppress(PackageNotFoundError):
__version__ = version(__name__)


def __getattr__(name: str) -> type:
if name in ("AsyncAnticaptchaClient", "AsyncJob"):
from .async_client import AsyncAnticaptchaClient, AsyncJob

globals()["AsyncAnticaptchaClient"] = AsyncAnticaptchaClient
globals()["AsyncJob"] = AsyncJob
return globals()[name]
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


__all__ = [
"AnticaptchaClient",
"Job",
Expand All @@ -50,4 +61,6 @@
"AntiGateTask",
"AnticaptchaException",
"AnticatpchaException",
"AsyncAnticaptchaClient",
"AsyncJob",
]
Loading
Loading