Skip to content
Skill

Stealthii

Drive a stealth-patched Chromium from Python for HTTP fetching, JS rendering, screenshots, and full page interaction. Use for any web work that must survive bot detection.

Namestealthii
Files5 files, 17693 bytes

Instructions

Stealthii

One persistent, stealth-patched Chromium behind a Python async API.
Source of truth is the repo this skill was verified against:
https://retoor.molodetz.nl/retoor/stealthii at version 0.1.0.
Every recipe below was executed live during verification.

What it is not

Stealthii is not a Playwright clone. Its primary API is shaped like
aiohttp/httpx (get, post, render, screenshot, download).
Playwright knowledge transfers only inside page(), which yields a real
Playwright Page for interaction the high-level API does not cover.

Install

pip install git+https://retoor.molodetz.nl/retoor/stealthii.git
playwright install chromium

Exactly one runtime dependency: playwright>=1.40.0.

Lifecycle

import asyncio
from stealthii import Stealthii

async def main():
    stealth = Stealthii()
    await stealth.start()
    try:
        resp = await stealth.get("https://example.com")
        print(resp.status, len(await resp.read()))
    finally:
        await stealth.shutdown()

asyncio.run(main())

One instance owns one Chromium process for the whole application lifetime.
Share it; never construct one per request. An instance binds to the first
event loop that uses it; reuse from another loop raises
StealthUnavailableError at once instead of hanging.

The ladder

Always start at the cheapest rung that can do the job and escalate one
rung at a time.

Rung 1, fast HTTP path. No tab, no JS, through the browser's own network
stack with a genuine Chrome TLS fingerprint. Default for fetching pages,
APIs, feeds, and files.

resp = await stealth.get(url, params={"q": "x"}, timeout=15)
resp.status
await resp.text()
await resp.json()
await stealth.post(url, json={"hello": "world"})
await stealth.post(url, data={"field": "value"}, files={"up": ("n.txt", b"data")})
raw = await stealth.download("https://example.com/photo.png")

Rung 2, full render. A real page load with JS execution for SPAs, search
results rendered client-side, and interstitial challenges.

result = await stealth.render(url, collect_links=True)
result["status"]
result["text"]
result["links"]

Rung 3, raw page. A stealth-patched Playwright Page for clicking,
typing, waiting, and anything else interactive. The tab always closes,
even when your code raises.

async with stealth.page() as pg:
    await pg.goto(url)
    await pg.click("#accept-cookies")
    await pg.fill("#search", "query")
    await pg.screenshot(path="after.png")

async with stealth as pg claims a tab the same way and is safe under
asyncio.gather: concurrent tasks each get their own tab.

Screenshots

Fresh capture of a URL, own tab, PNG:

path = await stealth.screenshot(url, path="shot.png", full_page=True)
b64 = await stealth.screenshot(url)

Screenshot of a page you already interacted with: call pg.screenshot()
inside page(). stealth.screenshot(url) always navigates itself and
cannot see another tab's state.

Live capture contract. While a browser session runs, keep one current
frame at a stable path so the host page can display it:

python scripts/live_shot.py "https://example.com" --path live.png

The script rewrites the file only when the PNG bytes actually changed
(SHA-256 comparison) and prints JSON with changed, path, and
sha256. The host refreshes its view only on changed: true, so call
it on a steady cadence without worrying about redundant refreshes.

Sessions

Anonymous calls share one recycled context with no cookie continuity.
Pass session="name" to any method for a dedicated context whose
cookies persist across calls in both directions:

await stealth.render("https://example.com/login", session="acme")
resp = await stealth.get("https://example.com/api/data", session="acme")

Warm up a hard target with render(), then reuse its cookies with the
fast path. Sessions never share cookies with each other. Contexts
recycle after 500 requests or 30 minutes by default; the browser itself
relaunches every 6 hours. Tune at construction, never per call.

Errors

Retries are bounded by max_retries (default 3) with a short backoff.
Catch StealthRequestError per URL, record the failure, continue with
the next target. Never retry past the cap in your own loop.

Limits you must respect

Guardrails

References

Open only what you need:

Files

Download ZIP

10.8ms