Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ permissions:
contents: read

jobs:
# Reuse the full lint + test workflow so a release never ships from a
# broken tree, even if the tag was pushed without a CI run on the commit.
test:
uses: ./.github/workflows/tests.yml

# Build sdist and wheel once and hand them to the publish job as an
# artifact, so what gets uploaded to PyPI is exactly what was built here.
# Also fails fast if the git tag disagrees with _version.py.
build:
needs: test
runs-on: ubuntu-latest
Expand Down Expand Up @@ -46,10 +51,13 @@ jobs:
name: dist
path: dist/

# Upload the built artifacts to PyPI via trusted publishing, then create
# the matching GitHub Release with auto-generated notes and the same
# sdist/wheel attached. Only runs on an actual version tag --
# workflow_dispatch runs stop at the build/test stage so you can dry-run
# the release without shipping it.
publish:
needs: build
# Only publish on an actual version tag -- workflow_dispatch runs stop at
# the build/test stage so you can dry-run the release without shipping it.
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
environment:
Expand All @@ -59,6 +67,8 @@ jobs:
# Required for PyPI's trusted publishing (OIDC) -- no API token/secret needed.
# Configure this repo as a trusted publisher at https://pypi.org/manage/project/captcha-solver-api/settings/publishing/
id-token: write
# Required to create the GitHub Release.
contents: write
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
Expand All @@ -68,3 +78,11 @@ jobs:

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
files: dist/*
# Requires Discussions enabled in the repo with this category present.
discussion_category_name: Announcements
31 changes: 31 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,43 @@
name: Tests

on:
# Run on pushes to main only; feature branches are covered by the
# pull_request trigger, so this avoids a duplicate run per commit.
push:
branches: [main]
pull_request:
workflow_call:

jobs:
# Static checks: ruff lint + formatting and mypy type checking. Runs once
# on a single Python version since the result does not depend on the
# interpreter.
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install package with dev dependencies
run: python -m pip install -e ".[dev]"

- name: Ruff lint
run: ruff check .

- name: Ruff format check
run: ruff format --check .

- name: mypy
run: mypy captcha_solver_api

# Test suite across all supported Python versions, plus a build check
# to catch packaging errors early. Skipped if lint fails to save CI minutes.
test:
needs: lint
runs-on: ubuntu-latest
strategy:
matrix:
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
[![Tests](https://github.com/captcha-solver-api/python-sdk/actions/workflows/tests.yml/badge.svg)](https://github.com/captcha-solver-api/python-sdk/actions/workflows/tests.yml)
[![Typed](https://img.shields.io/badge/typing-typed-blue)](https://github.com/captcha-solver-api/python-sdk/blob/main/captcha_solver_api/py.typed)
[![License: MIT](https://img.shields.io/badge/license-MIT-green)](https://github.com/captcha-solver-api/python-sdk/blob/main/LICENSE.md)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)
[![GitHub Release](https://img.shields.io/github/v/release/captcha-solver-api/python-sdk)](https://github.com/captcha-solver-api/python-sdk/releases)
[![JavaScript SDK](https://img.shields.io/badge/also_available-JavaScript_SDK-f7df1e?logo=javascript&logoColor=black)](https://github.com/captcha-solver-api/javascript-sdk)

Official Python SDK for the Captcha Solver API. Solve reCAPTCHA v2/v3, Cloudflare Turnstile, GeeTest, Yandex SmartCaptcha, Tencent, and image/click captchas with a single method call -- sync or async.
Expand Down
16 changes: 8 additions & 8 deletions captcha_solver_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,26 @@
result = client.solve(task)
"""

from .client import CaptchaClient
from ._version import __version__
from .async_client import AsyncCaptchaClient
from .client import CaptchaClient
from .exceptions import (
CaptchaError,
ApiError,
NetworkError,
CaptchaError,
CaptchaTimeoutError,
NetworkError,
TimeoutError,
ValidationError,
)

from ._version import __version__

__all__ = [
"CaptchaClient",
"ApiError",
"AsyncCaptchaClient",
"CaptchaClient",
"CaptchaError",
"ApiError",
"NetworkError",
"CaptchaTimeoutError",
"NetworkError",
"TimeoutError",
"ValidationError",
"__version__",
]
15 changes: 10 additions & 5 deletions captcha_solver_api/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
from ._version import __version__
from .exceptions import (
ApiError,
NetworkError,
CaptchaTimeoutError,
NetworkError,
ValidationError,
)

Expand Down Expand Up @@ -87,7 +87,7 @@ async def aclose(self) -> None:
(`async with AsyncCaptchaClient(...) as c:`) to have it closed automatically."""
await self._client.aclose()

async def __aenter__(self) -> "AsyncCaptchaClient":
async def __aenter__(self) -> AsyncCaptchaClient:
return self

async def __aexit__(self, *exc_info: Any) -> None:
Expand All @@ -113,6 +113,8 @@ async def _request(
except ValueError as exc:
raise NetworkError(f"Non-JSON response from API: {response.text[:200]!r}") from exc

if not isinstance(data, dict):
raise NetworkError(f"Unexpected API response shape: {type(data).__name__}")
return data

def _ensure_success(self, data: Dict[str, Any]) -> None:
Expand Down Expand Up @@ -154,7 +156,7 @@ async def create_task(self, task: Any, language_pool: Optional[str] = None) -> i

data = await self._request("createTask", payload)
self._ensure_success(data)
return data["taskId"]
return int(data["taskId"])

async def get_task_result(self, task_id: int) -> Dict[str, Any]:
"""Fetch the current status of a previously submitted task.
Expand Down Expand Up @@ -203,7 +205,7 @@ async def get_balance(self) -> float:
payload = {"clientKey": self.client_key}
data = await self._request("getBalance", payload)
self._ensure_success(data)
return data["balance"]
return float(data["balance"])

async def solve(
self,
Expand Down Expand Up @@ -248,6 +250,9 @@ async def solve(
result = await self.get_task_result(task_id)

if result.get("status") == "ready":
return result["solution"]
solution = result["solution"]
if not isinstance(solution, dict):
raise NetworkError("API returned a ready task without a solution object")
return solution

raise CaptchaTimeoutError("Task solving timed out.")
15 changes: 10 additions & 5 deletions captcha_solver_api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
from ._version import __version__
from .exceptions import (
ApiError,
NetworkError,
CaptchaTimeoutError,
NetworkError,
ValidationError,
)

Expand Down Expand Up @@ -76,7 +76,7 @@ def close(self) -> None:
to have it closed automatically."""
self.session.close()

def __enter__(self) -> "CaptchaClient":
def __enter__(self) -> CaptchaClient:
return self

def __exit__(self, *exc_info: Any) -> None:
Expand All @@ -102,6 +102,8 @@ def _request(
except ValueError as exc:
raise NetworkError(f"Non-JSON response from API: {response.text[:200]!r}") from exc

if not isinstance(data, dict):
raise NetworkError(f"Unexpected API response shape: {type(data).__name__}")
return data

def _ensure_success(self, data: Dict[str, Any]) -> None:
Expand Down Expand Up @@ -141,7 +143,7 @@ def create_task(self, task: Any, language_pool: Optional[str] = None) -> int:

data = self._request("createTask", payload)
self._ensure_success(data)
return data["taskId"]
return int(data["taskId"])

def get_task_result(self, task_id: int) -> Dict[str, Any]:
"""Fetches the current status of a task created with `create_task()`.
Expand Down Expand Up @@ -187,7 +189,7 @@ def get_balance(self) -> float:
payload = {"clientKey": self.client_key}
data = self._request("getBalance", payload)
self._ensure_success(data)
return data["balance"]
return float(data["balance"])

def solve(
self,
Expand Down Expand Up @@ -230,6 +232,9 @@ def solve(
result = self.get_task_result(task_id)

if result.get("status") == "ready":
return result["solution"]
solution = result["solution"]
if not isinstance(solution, dict):
raise NetworkError("API returned a ready task without a solution object")
return solution

raise CaptchaTimeoutError("Task solving timed out.")
8 changes: 0 additions & 8 deletions captcha_solver_api/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,14 @@
class CaptchaError(Exception):
"""Base exception for all SDK errors."""

pass


class NetworkError(CaptchaError):
"""Raised when a network request fails."""

pass


class CaptchaTimeoutError(CaptchaError):
"""Raised when the operation exceeds the configured timeout."""

pass


# Deprecated alias kept for backward compatibility. It shadows the built-in
# ``TimeoutError`` when imported by name, so prefer ``CaptchaTimeoutError``.
Expand All @@ -37,5 +31,3 @@ def __init__(self, error_code: str, error_description: str) -> None:

class ValidationError(CaptchaError):
"""Raised for client-side argument problems caught before any request is sent."""

pass
2 changes: 1 addition & 1 deletion captcha_solver_api/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from __future__ import annotations

from typing import Any, Dict, List, Optional
from typing import Any, Dict, Optional


class BaseTask:
Expand Down
8 changes: 4 additions & 4 deletions examples/async/balance.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
try:
from dotenv import load_dotenv
except ModuleNotFoundError as exc:
if exc.name != 'dotenv':
if exc.name != "dotenv":
raise
load_dotenv = None

repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
sys.path.append(repo_root)
if load_dotenv is not None:
load_dotenv(os.path.join(repo_root, '.env'))
load_dotenv(os.path.join(repo_root, ".env"))

from captcha_solver_api import AsyncCaptchaClient

Expand All @@ -30,7 +30,7 @@
# you can just set the API key directly to its value like:
# api_key="1abc234de56fab7c89012d34e56fa7b8"

api_key = os.getenv('CAPTCHA_API_KEY', 'YOUR_API_KEY')
api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY")

solver = AsyncCaptchaClient(api_key)

Expand All @@ -40,7 +40,7 @@ async def main():
# Returns a float with the available amount in your account currency.
try:
balance = await solver.get_balance()
print('Balance: ' + str(balance))
print("Balance: " + str(balance))
except Exception as e:
sys.exit(e)

Expand Down
Loading
Loading