Legibl docs PHP 0.1.0-dev · Python 0.1.0

Python quickstart

Describes Python SDK 0.1.0 (legibl.__version__). Requires Python 3.8 or later. The SDK itself has no runtime dependencies — it integrates with whichever HTTP client you already use rather than bringing its own.

Install

The package is not yet on PyPI. Install it from a checkout, with the extra matching the client you use:

pip install '/path/to/legibl/sdk/python[httpx]'      # or [requests]

The extras only pull in the HTTP client. pip install /path/to/legibl/sdk/python with no extra installs the detection and solver code alone, which is enough if you drive the API yourself.

Configure

export LEGIBL_API_KEY=your-key

With no key set the SDK runs in stub mode: it returns a synthetic token and spends nothing. It also drops to stub mode when it detects a test run — pytest or unittest imported, PYTEST_CURRENT_TEST set, or APP_ENV/ENVIRONMENT equal to test — because a suite that quietly bills a live solver on every run is a common and expensive accident. Override with LEGIBL_ALLOW_LIVE_IN_TESTS=1.

Solve a captcha with httpx

four lines

import httpx
from legibl.transport.httpx_transport import LegiblTransport

with httpx.Client(transport=LegiblTransport()) as client:
    body = client.get("https://example.test/protected").text

LegiblTransport wraps whatever transport httpx would otherwise use, so TLS, proxies, retries and connection pooling remain httpx's concern. It takes an optional wrapped= transport if you already have a configured one.

Solve a captcha with requests

five lines

import requests
from legibl.transport.requests_adapter import LegiblAdapter

session = requests.Session()
session.mount("https://", LegiblAdapter())
body = session.get("https://example.test/protected").text

LegiblAdapter is an HTTPAdapter subclass, mounted on a session the ordinary way, so pooling, retries and TLS remain requests' concern. Mount it on "http://" as well if you talk to plain-HTTP targets.

Both integrations use public hooks

A custom transport for httpx and an HTTPAdapter subclass for requests — never a fork, never a monkey-patch. Public hooks are stable API and internals are not, and that difference is the whole maintenance budget.

What gets inspected

Only bodies that can carry a challenge are examined. The requests adapter skips any response whose Content-Type contains neither html nor text; the httpx transport passes through any body that will not decode as text. Binary payloads are never decoded speculatively.

Confirm it is actually working

The SDK is deliberately quiet, so a broken integration and a clean run look identical. Attach an observer:

from legibl.config import Config
from legibl.interceptor import Interceptor
from legibl.observer import CallbackObserver
from legibl.transport.httpx_transport import LegiblTransport

observer = CallbackObserver(
    lambda outcome, domain, secs: print(outcome.status, domain, round(secs, 2), outcome.reason),
    lambda challenge, reason: print("solve failed:", challenge.captcha_type, reason),
)

transport = LegiblTransport(Interceptor.from_config(Config.from_environment(), observer))

The second callback is the one that matters when a solve does not happen: it carries the service's errorCode in its message, and without an observer that detail is discarded.

Log the resolved configuration too — it is built to be safe to log, reporting api_key_set as a boolean and proxy domains only, never credentials:

print(Config.from_environment().describe())
# {'mode': 'live', 'service_url': 'https://api.legibl.io', 'api_key_set': True, ...}

Detection without interception

If you drive requests yourself, the detector is a pure function over HTML. It never touches the network and never returns a token — tokens are issued by the challenge provider and cannot be derived from a saved page.

from legibl import detect

result = detect(html, "https://example.test/protected")

result.extractable    # bool — False is a normal outcome, not an error
result.captcha_type   # recaptcha_v2 | recaptcha_v2_invisible | turnstile | image_ocr
result.sitekey
result.pageurl
result.invisible      # reCAPTCHA only — never set for Turnstile
result.submit_field
result.action         # Turnstile
result.cdata          # Turnstile
result.reason         # why, when extractable is False

extractable is False with a captcha_type set means a challenge was correctly identified and is out of reach of form injection. Treating that as “no captcha here” is how a target silently stops working — see unreachable challenges.

Next