Skip to content

Commit 3a7840e

Browse files
ad-mclaude
andauthored
Add async support with AsyncAnticaptchaClient (#131)
* Add async support with AsyncAnticaptchaClient and AsyncJob Add a new async_client module providing AsyncAnticaptchaClient and AsyncJob classes that mirror the sync API but use httpx.AsyncClient and asyncio.sleep for non-blocking operation in async frameworks (FastAPI, aiohttp, etc.). This is a non-breaking change: the sync API is untouched, httpx remains an optional dependency (pip install python-anticaptcha[async]), and the new classes are lazily imported in __init__.py to avoid requiring httpx at package import time. https://claude.ai/code/session_01Pimg4VAco2v4srPeZj44Zm * Extract shared mixins, add documentation and examples for async support - Extract _BaseClientMixin and _BaseJobMixin into _common.py to share init logic, payload builders, solution getters, and repr between sync and async clients - Refactor AnticaptchaClient and AsyncAnticaptchaClient to inherit from shared mixins, reducing maintenance burden - Add "Async Usage" section to README.md and docs/usage.rst - Add async_client automodule to docs/api.rst - Add CHANGELOG.rst entries for async support - Add async example scripts (async_recaptcha_request.py, async_balance.py) - Add "Framework :: AsyncIO" and "Topic :: Internet :: WWW/HTTP" classifiers - Add "asyncio", "async", "httpx" keywords for PyPI discoverability https://claude.ai/code/session_01Pimg4VAco2v4srPeZj44Zm * Balance sync and async as equal first-class citizens - Rename base.py → sync_client.py for symmetry with async_client.py; add backward-compat base.py re-export shim - Rename all sync example files with sync_ prefix to match async_ examples - Rename test_base.py → test_sync_client.py - Promote "Async Usage" to same heading level as sync in README and docs - Add "Sync client" heading in docs/usage.rst for symmetry - Rename "Base" → "Sync Client" in docs/api.rst - Update __init__.py to import from sync_client instead of base - All existing import paths preserved for backward compatibility https://claude.ai/code/session_01Pimg4VAco2v4srPeZj44Zm * Defer httpx ImportError to instantiation time for Sphinx compatibility The module-level ImportError prevented Sphinx autodoc from importing async_client when httpx is not installed. Now the module imports successfully (setting httpx=None) and raises ImportError only when AsyncAnticaptchaClient is instantiated without httpx. https://claude.ai/code/session_01Pimg4VAco2v4srPeZj44Zm * Fix CI: linter errors, mypy types, and add httpx to test deps - Fix ruff import sorting in __init__.py, test_sync_client.py, test_async_client.py - Fix ruff-format: async_client.py await parenthesization, string concatenation - Fix mypy: add return type to __getattr__, add type:ignore for optional httpx import - Add httpx to test dependencies so async tests work in CI - Remove redundant explicit imports from base.py shim (covered by *) https://claude.ai/code/session_01Pimg4VAco2v4srPeZj44Zm * Fix E2E tests: update example imports to use sync_ prefix The example files were renamed with sync_ prefix but test_examples.py still imported them by their old names. https://claude.ai/code/session_01Pimg4VAco2v4srPeZj44Zm * Remove import aliases in test_examples.py, use sync_ prefixed names directly https://claude.ai/code/session_01Pimg4VAco2v4srPeZj44Zm * Add six to test dependencies Required by funcaptcha and hcaptcha example tests. https://claude.ai/code/session_01Pimg4VAco2v4srPeZj44Zm --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f6d348c commit 3a7840e

29 files changed

Lines changed: 982 additions & 310 deletions

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ Unreleased
77
Added
88
#####
99

10+
- Add ``AsyncAnticaptchaClient`` and ``AsyncJob`` for async/await usage with ``httpx`` (``pip install python-anticaptcha[async]``)
11+
- Rename ``base.py`` → ``sync_client.py`` for symmetry with ``async_client.py``; backward-compatible ``base.py`` shim preserved
12+
- Rename sync example files with ``sync_`` prefix to match ``async_`` examples
1013
- Add context manager support to ``AnticaptchaClient`` (``__enter__``, ``__exit__``, ``close``)
1114
- Add ``ANTICAPTCHA_API_KEY`` environment variable fallback for ``AnticaptchaClient``
1215
- Add ``Proxy`` frozen dataclass with ``parse_url()`` and ``to_kwargs()`` methods

README.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ Install as standard Python package using:
2020
pip install python-anticaptcha
2121
```
2222

23+
For async support (FastAPI, aiohttp, Starlette, etc.):
24+
25+
```
26+
pip install python-anticaptcha[async]
27+
```
28+
2329
## Usage
2430

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

51+
## Async Usage
52+
53+
For async frameworks, use `AsyncAnticaptchaClient` — the API mirrors the sync
54+
client but all methods are awaitable:
55+
56+
```python
57+
from python_anticaptcha import AsyncAnticaptchaClient, NoCaptchaTaskProxylessTask
58+
59+
api_key = '174faff8fbc769e94a5862391ecfd010'
60+
site_key = '6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-'
61+
url = 'https://www.google.com/recaptcha/api2/demo'
62+
63+
async with AsyncAnticaptchaClient(api_key) as client:
64+
task = NoCaptchaTaskProxylessTask(url, site_key)
65+
job = await client.create_task(task)
66+
await job.join()
67+
print(job.get_solution_response())
68+
```
69+
70+
The full integration example is available in file `examples/async_recaptcha_request.py`.
71+
72+
## Sync Usage
73+
4574
### Solve recaptcha
4675

4776
Example snippet for Recaptcha:
@@ -60,7 +89,7 @@ job.join()
6089
print(job.get_solution_response())
6190
```
6291

63-
The full integration example is available in file `examples/recaptcha_request.py`.
92+
The full integration example is available in file `examples/sync_recaptcha_request.py`.
6493

6594
If you process the same page many times, to increase reliability you can specify
6695
whether the captcha is visible or not. This parameter is not required, as the

docs/api.rst

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
API
22
===
33

4-
Base
5-
----
4+
Sync Client
5+
-----------
66

7-
.. automodule:: python_anticaptcha.base
7+
.. automodule:: python_anticaptcha.sync_client
8+
:members:
9+
:undoc-members:
10+
11+
Async Client
12+
------------
13+
14+
.. automodule:: python_anticaptcha.async_client
815
:members:
916
:undoc-members:
1017

docs/usage.rst

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,35 @@ The client can be used as a context manager to ensure the underlying session is
2121
job = client.create_task(task)
2222
job.join()
2323
24+
Async client
25+
############
26+
27+
.. note::
28+
29+
Requires the ``async`` extra: ``pip install python-anticaptcha[async]``
30+
31+
For async frameworks (FastAPI, aiohttp, Starlette) use ``AsyncAnticaptchaClient`` —
32+
the API mirrors the sync client but all methods are awaitable:
33+
34+
.. code:: python
35+
36+
from python_anticaptcha import AsyncAnticaptchaClient, NoCaptchaTaskProxylessTask
37+
38+
api_key = '174faff8fbc769e94a5862391ecfd010'
39+
site_key = '6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-'
40+
url = 'https://www.google.com/recaptcha/api2/demo'
41+
42+
async with AsyncAnticaptchaClient(api_key) as client:
43+
task = NoCaptchaTaskProxylessTask(url, site_key)
44+
job = await client.create_task(task)
45+
await job.join()
46+
print(job.get_solution_response())
47+
48+
The full integration example is available in file ``examples/async_recaptcha_request.py``.
49+
50+
Sync client
51+
###########
52+
2453
Solve recaptcha
2554
###############
2655

@@ -40,7 +69,7 @@ Example snippet for Recaptcha:
4069
job.join()
4170
print(job.get_solution_response())
4271
43-
The full integration example is available in file ``examples/recaptcha_request.py``.
72+
The full integration example is available in file ``examples/sync_recaptcha_request.py``.
4473

4574
If you process the same page many times, to increase reliability you can specify
4675
whether the captcha is visible or not. This parameter is not required, as the

examples/async_balance.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import asyncio
2+
from os import environ
3+
from pprint import pprint
4+
5+
from python_anticaptcha import AsyncAnticaptchaClient
6+
7+
api_key = environ["KEY"]
8+
9+
10+
async def process():
11+
async with AsyncAnticaptchaClient(api_key) as client:
12+
balance = await client.get_balance()
13+
pprint(balance)
14+
15+
16+
if __name__ == "__main__":
17+
asyncio.run(process())
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import asyncio
2+
import re
3+
from os import environ
4+
5+
import httpx
6+
7+
from python_anticaptcha import AsyncAnticaptchaClient, NoCaptchaTaskProxylessTask
8+
9+
api_key = environ["KEY"]
10+
site_key_pattern = 'data-sitekey="(.+?)"'
11+
url = "https://www.google.com/recaptcha/api2/demo?invisible=false"
12+
EXPECTED_RESULT = "Verification Success... Hooray!"
13+
14+
15+
async def get_form_html(session: httpx.AsyncClient) -> str:
16+
return (await session.get(url)).text
17+
18+
19+
async def get_token(client: AsyncAnticaptchaClient, form_html: str) -> str:
20+
site_key = re.search(site_key_pattern, form_html).group(1)
21+
task = NoCaptchaTaskProxylessTask(website_url=url, website_key=site_key)
22+
job = await client.create_task(task)
23+
await job.join()
24+
return job.get_solution_response()
25+
26+
27+
async def form_submit(session: httpx.AsyncClient, token: str) -> str:
28+
return (await session.post(url, data={"g-recaptcha-response": token})).text
29+
30+
31+
async def process():
32+
async with AsyncAnticaptchaClient(api_key) as client, httpx.AsyncClient() as session:
33+
html = await get_form_html(session)
34+
token = await get_token(client, html)
35+
return await form_submit(session, token)
36+
37+
38+
if __name__ == "__main__":
39+
result = asyncio.run(process())
40+
assert "Verification Success... Hooray!" in result

0 commit comments

Comments
 (0)