Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

Lithium

Version – 1.0b2.post1

An HTTP-first, high stealth web runtime for Python. The simplicity of requests + the interaction model of Selenium, backed by a tiny native Go runtime — no browser to install, no drivers, no config to get going.

from lithium import Client

client = Client()                      # zero-config, undetected out of the box
page = client.get("https://example.com/login")

page.fill("#username", "alice")
page.fill("#password", "secret")
page.click("button[type=submit]")
page.wait_for("#dashboard")

print(page.text)
page.screenshot("dashboard.png")

Fun fact #1: the name comes from lithium — the lightest solid element — because the whole runtime binary is only ~19 MB. Fun fact #2: Python talks to the Go runtime over a Unix socket using MessagePack. No browser process, no WebDriver, no Chromium. Just a runtime. Fun fact #3: by default the runtime speaks a Chrome TLS (JA3) fingerprint over uTLS, so Client() is already impersonating a real browser — you don't have to turn anything on.


Installation

pip install lithium-web

That's it.


Building & publishing to PyPI (maintainers)

The wheel must bundle the static Go runtime binary — if you skip the build step the wheel has no runtime and won't work (RuntimeNotStarted).

# 1. install build deps
pip install build twine cython

# 2. cross-compile the static Go binary + build the wheel (this is what bundles it)
./build_release.sh            # fat wheel (all os/arch), source visible
# ./build_cython.sh           # optional: obfuscated wheel (current platform only)

# 3. sanity-check the binary actually got bundled
unzip -l python/dist/*.whl | grep 'lithium/bin/'   # must list lithium-runtime-* files

# 4. upload (test first, then real)
twine upload --repository testpypi python/dist/*.whl
twine upload python/dist/*.whl

Before publishing, confirm the wheel isn't missing the binary and that version in python/pyproject.toml matches what you intend to ship.


Quickstart — major features, individually

Requests-style HTTP

from lithium import Client

client = Client()
r = client.get("https://api.example.com/users?limit=10")
print(r.status_code)          # 200
print(r.ok)                   # True
print(r.json())               # parsed body

# POST with JSON
r = client.post("https://api.example.com/users", json={"name": "Ada"})

# POST with a form body (auto urlencoded)
r = client.post("https://httpbin.org/post", data={"q": "lithium"})

# POST multipart upload
r = client.post("https://httpbin.org/post",
                data={"note": "hi"},
                files={"file": ("report.pdf", b"%PDF-1.4 ...")})

Selenium-style page interaction

from lithium import Client

page = client.get("https://example.com/login")
page.fill("#username", "alice")
page.fill("#password", "secret")
page.click("button[type=submit]")     # navigates; `page` updates in place
page.wait_for("#dashboard")           # waits for the element to appear
print(page.text)                      # text of the current page

Execute JavaScript

page = client.get("https://example.com")
page.execute_script("document.body.dataset.loaded = 'true'")
print(page.dom.query("body").get_attr("data-loaded"))   # 'true'

Query the DOM

page = client.get("https://example.com/products")
items = page.dom.query_all(".product")
for el in items:
    print(el.text, el.get_attr("data-price"))

Screenshots

Real HTML/CSS rendering via WeasyPrint (no browser) — real text, fonts, colors, and layout:

page = client.get("https://example.com")
page.screenshot("page.png")                     # writes a PNG, returns the path
png_bytes = page.screenshot()                   # or get raw bytes
page.screenshot("w.png", width=1200, scale=2.0) # custom width / crispness

Async client

import asyncio
from lithium import AsyncClient

async def main():
    async with AsyncClient() as client:
        r = await client.get("https://example.com")
        print(r.status_code)

asyncio.run(main())

Streaming downloads

client = Client()
client.stream_to_file("https://example.com/bigfile.bin", "bigfile.bin")

Proxies & rotating proxies

# single proxy
client = Client(proxy="socks5://user:pass@host:1080")

# rotating proxy pool (round-robin per request)
client = Client(proxies=[
    "socks5://u:p@proxy1:1080",
    "socks5://u:p@proxy2:1080",
])

Custom CA & mutual TLS

client = Client(
    ca_cert="/path/to/ca.pem",
    client_cert="/path/to/client.crt",
    client_key="/path/to/client.key",
)

Geo / locale auto-detection

client = Client(auto_location=True)     # timezone + currency + country
r = client.get("https://example.com")
print(r.timezone, r.currency, r.country_code)

Network inspection

page = client.get("https://example.com")
for req in page.network.requests:        # every HTTP + in-page fetch/XHR
    print(req.method, req.status, req.url)

Custom browser profile / fingerprint

# pick a coherent identity (UA + JA3 + navigator + GPU all agree)
client = Client(platform="windows", browser="chrome", fingerprint="my-identity")

One script that uses everything

import asyncio, json
from lithium import Client, AsyncClient

# 1. Requests-style client, undetected by default
client = Client(proxies=["socks5://u:p@h1:1080", "socks5://u:p@h2:1080"])
client.get("https://example.com/set-session")

# 2. A page we interact with, like Selenium
page = client.get("https://example.com/login")
page.fill("#username", "alice")
page.fill("#password", "secret")
page.check("#terms")
page.click("button[type=submit]")
page.wait_for("#dashboard")
print("landed:", page.status_code)

# 3. DOM + JS
rows = page.dom.query_all("table tbody tr")
page.execute_script("document.title = 'hello from js'")

# 4. Run JS that does real network (in-page fetch)
page.execute_script(
    "fetch('/api/data').then(r=>r.json())"
    ".then(d=>document.body.setAttribute('data-h', d.hello));"
)

# 5. Rich media + screenshot
print("media:", page.media)            # discovered video/audio/img sources
page.screenshot("dashboard.png")

# 6. Inspect every request the session made (HTTP + JS-driven)
for req in page.network.requests:
    print("  net:", req.method, req.status, req.url)

# 7. Large download, streamed to disk
client.stream_to_file("https://example.com/db.sqlite", "db.sqlite")

# 8. Plain HTTP / JSON when you don't need a page
r = client.post("https://example.com/api", json={"a": 1})
print(r.json())

# 9. Async when you need concurrency
async def main():
    async with AsyncClient() as ac:
        results = await asyncio.gather(*[ac.get("https://example.com") for _ in range(5)])
        print([r.status_code for r in results])
asyncio.run(main())

Config reference

All options work as direct keyword args to Client(...) or via a Config/JSON file.

Option Default What it does
profile "chrome-windows" Coherent browser identity (UA + headers + JA3 + navigator).
fingerprint None Persistent identity name; same name = same profile across runs.
platform None Force windows / macos / linux / android / ios.
browser None Force chrome / firefox / opera / brave.
proxy None Single proxy URL (http/https/socks4/socks4a/socks5/socks5h).
proxies None List of proxy URLs, rotated round-robin per request.
verify_ssl True Verify server certificates.
ca_cert, client_cert, client_key None Custom CA bundle and client certificate (mTLS).
trust_env True Respect HTTP_PROXY/HTTPS_PROXY/NO_PROXY env vars.
tls_spoof None Explicitly enable/disable TLS (JA3) impersonation.
http3 False Try HTTP/3 (QUIC) with graceful h1/h2 fallback.
execute_scripts False Auto-run inline <script> on page load.
js_timeout 10.0 Max seconds a JS call may run.
timeout 30.0 Default request timeout (seconds).
max_redirects 0 Redirect limit (0=none, N>0=follow up to N).
max_retries 0 Auto-retry count for transient errors (429/5xx).
retry_backoff 0.5 Seconds between retries (doubles each attempt).
retry_codes (429,500,502,503,504) Status codes that trigger a retry.
auth None (user, pass) tuple for HTTP Basic auth on every request.
max_idle_conns, max_idle_conns_per_host 100, 10 HTTP connection-pool sizing.
viewport_width, viewport_height platform default Screen/inner size (screen, innerWidth, matchMedia).
device_scale_factor platform default devicePixelRatio (mobile auto-sets to 2).
screen_color_depth 24 screen.colorDepth / pixelDepth.
hardware_concurrency, device_memory from fingerprint navigator.hardwareConcurrency / .deviceMemory.
max_touch_points 0 navigator.maxTouchPoints.
do_not_track None navigator.doNotTrack.
audio_sample_rate 44100 AudioContext().sampleRate.
screenshot_width, screenshot_height 1280, None Page size (width/height) for page.screenshot().
virtual_gpu True Enable virtual WebGL context.
canvas_noise True Make toDataURL vary per identity.
auto_location, auto_timezone, auto_currency, auto_language None Auto-detect locale from your IP (via geo_url).
geo_url ip-api.com Custom geo lookup endpoint.
storage_file None Persist localStorage across runs/sessions.
socket_timeout None Seconds to wait for a reply from the runtime.
shared True Reuse one runtime process across clients.
binary auto Path to the runtime binary (override).
verbose False Print go-side logs to stderr.
solution False Attach live DNS/TCP/TLS diagnostics to errors.

Browser runtime — every Web API

page.execute_script(js) runs real JavaScript against the page's live DOM, and a large slice of the browser API surface is implemented. Here is each API and how to use it.

In the examples below, page = client.get("https://example.com") gives you a live page whose DOM and JS state persist across execute_script calls.

console & timers

page = client.get("https://example.com")
page.execute_script("console.log('hello from js')")
page.execute_script("setTimeout(() => document.body.dataset.t = '1', 50)")
page.execute_script("requestAnimationFrame(() => document.body.dataset.raf = '1')")

TextEncoder / TextDecoder

# crypto.subtle.digest needs an ArrayBuffer; these encode/decode it
page.execute_script("window.__enc = new TextEncoder().encode('hi').length")   # 2
page.execute_script("window.__dec = new TextDecoder().decode(new Uint8Array([104,105]))")  # "hi"

WebCrypto

page.execute_script("""
  window.__rv = crypto.randomUUID();                       // e.g. "8f...-...."
  var b = new Uint8Array(8); crypto.getRandomValues(b);    // random bytes
  window.__len = b.length;
  crypto.subtle.digest('SHA-256', new TextEncoder().encode('hi'))
    .then(function(buf){ window.__digest = new Uint8Array(buf).length; });   // 32
""")
print(page.execute_script("window.__digest"))   # "32"

performance

page.execute_script("window.__now = typeof performance.now() === 'number'")
page.execute_script("window.__mem = typeof performance.memory")   # "object" on chrome-family

matchMedia

page.execute_script("matchMedia('(min-width: 100px)').matches")     # "true"
page.execute_script("matchMedia('(prefers-color-scheme: dark)').matches")  # "false"

getComputedStyle

page = client.get("https://example.com")
page.execute_script("document.body.setAttribute('style','display: flex')")
page.execute_script("window.getComputedStyle(document.body).display")  # "flex"

Fonts (document.fonts — realistic, per-identity)

Real browsers expose the OS font list; this runtime ships a curated per-platform list with a deterministic subset per identity, probed via document.fonts.check:

page.execute_script('document.fonts.check("12px Arial")')          # True (common font)
page.execute_script('document.fonts.check("12px Consolas")')       # varies by identity, stable
page.execute_script('document.fonts.check("12px NotARealFont")')   # False
page.execute_script("document.fonts.status")                       # "loaded"

# each identity gives a distinct-but-stable font signature
page.execute_script("JSON.stringify([...document.fonts.values()].length)")  # count of "installed"

navigator — a fully coherent profile

page.execute_script("JSON.stringify({ua:navigator.userAgent, platform:navigator.platform, vendor:navigator.vendor})")
# hardware, memory, plugins, languages, javaEnabled, sendBeacon, doNotTrack, getBattery, permissions...
page.execute_script("navigator.hardwareConcurrency")   # 8
page.execute_script("navigator.javaEnabled()")         # "false"

screen & window

page.execute_script("JSON.stringify([screen.width, screen.height, screen.colorDepth, devicePixelRatio])")
# e.g. [1920, 1080, 24, 1] on desktop, [390, 844, 24, 2] on a mobile fingerprint

Storage — localStorage / sessionStorage

page.execute_script("localStorage.setItem('greeting','hi')")
page.execute_script("localStorage.getItem('greeting')")   # "hi"
# sessionStorage is per-session; set storage_file=... to persist localStorage across runs

Network from JS — fetch() & XMLHttpRequest (real HTTP)

page.execute_script("""
  fetch('/api/data').then(r => r.json())
    .then(d => document.body.setAttribute('data-hello', d.hello));
""")
page.execute_script("""
  var x = new XMLHttpRequest();
  x.open('GET', '/api/data');
  x.onload = function(){ document.body.setAttribute('data-x', x.responseText); };
  x.send();
""")

MutationObserver

page.execute_script("""
  window.__seen = null;
  var t = document.querySelector('body');
  new MutationObserver(function(recs){ window.__seen = recs[0].attributeName; })
    .observe(t, {attributes:true});
  t.setAttribute('data-flag','1');            // triggers the observer
""")
print(page.execute_script("window.__seen"))    # "data-flag"

WebSocket (real connection)

page.execute_script("""
  window.__msg = null;
  var ws = new WebSocket('wss://echo.websocket.org');
  ws.onopen = function(){ ws.send('ping'); };
  ws.onmessage = function(e){ window.__msg = e.data; };
""")

IndexedDB

page.execute_script("""
  window.__got = null;
  var req = indexedDB.open('db', 1);
  req.onupgradeneeded = function(){
    var db = req.result;
    if(!db.objectStoreNames.contains('k')) db.createObjectStore('k');
  };
  req.onsuccess = function(){
    var tx = req.result.transaction('k','readwrite').objectStore('k');
    tx.put('v','key');
    window.__got = 'ready';
  };
""")

History & location

page.execute_script("history.pushState({}, '', '/pushed?x=1')")
page.execute_script("location.pathname + location.search")   # "/pushed?x=1"

Permissions & Battery

page.execute_script("navigator.permissions.query({name:'geolocation'}).then(s=>window.__perm=s.state)")
page.execute_script("navigator.getBattery().then(b=>window.__battery=b.level)")

Canvas 2D

page.execute_script("""
  var c = document.createElement('canvas');
  var x = c.getContext('2d');
  x.fillStyle = 'red'; x.fillRect(0,0,10,10);
  c.toDataURL();                 // "data:image/png;base64,..." (varies per identity if canvas_noise)
""")

WebGL

page.execute_script("""
  var c = document.createElement('canvas');
  var gl = c.getContext('webgl');
  var ext = gl.getExtension('WEBGL_debug_renderer_info');
  JSON.stringify([gl.getParameter(ext.UNMASKED_VENDOR_WEBGL), gl.getParameter(ext.UNMASKED_RENDERER_WEBGL)]);
""")

AudioContext

page.execute_script("new AudioContext().sampleRate")        # "44100" (configurable)
page.execute_script("new OfflineAudioContext(1, 4096, 44100).startRendering()")

Media elements

page.execute_script("document.createElement('video').canPlayType('video/mp4; codecs=\"avc1.42E01E, mp4a.40.2\"')")  # "probably"
page.execute_script("document.createElement('video').canPlayType('video/mp4')")  # "maybe"

Element API

page.dom.query(sel) and page.dom.query_all(sel) return Element objects with these methods:

page = client.get("https://example.com")
el = page.dom.query("#content")            # or .query_all(".item")

el.get_attr("data-price", default="0")     # read an attribute
el.set_attr("data-price", "99")            # set an attribute
el.text                                   # trimmed text content
el.html                                   # outer HTML
el.remove()                               # remove from the DOM

# DOM events (Python callbacks)
def on_click(e): print("clicked", e["type"], e["target"])
el.add_event_listener("click", on_click)
el.dispatch_event("click")                 # fires the python listener

Interaction methods on page itself (Selenium-style) all accept a selector:

page.fill("#username", "alice")            # set input value (fires input/change)
page.type("#username", "ab", delay=0.05)   # type char-by-char (fires keydown/input/keyup)
page.click("button[type=submit]")          # click; navigates -> page updates in place
page.submit("#myform")                     # submit a form
page.select_option("select[name=color]", "blue")
page.check("#terms") / page.uncheck("#terms")
page.clear("#username")
page.wait_for("#dashboard", timeout=5.0)   # poll until the selector matches
page.goto("https://example.com/next")      # navigate; page updates in place

BeautifulSoup-style DOM

page.dom and every Element speak fluent BeautifulSoup — no second parser needed:

page = client.get("https://example.com")

page.title                                    # the <title> text
page.find("div")                              # first <div> in the document
page.dom.find("div", {"id": "content"})       # bs4 `.find(name, attrs=...)`
page.dom.find_all("p", class_="product")      # `.find_all` with class_=
page.dom.select(".product")                   # CSS selector (`.select`)
page.dom.select_one("#content")

el = page.dom.find("div")
el.name                                       # tag name
el.string / el.get_text()                     # text content
el.get("data-price", default="0")             # attribute access
el.has_attr("id")
el.parent / el.children / el.siblings         # tree traversal
el.next_sibling / el.previous_sibling
el.descendants                                # every descendant element
el.find("a") / el.find_all("span")            # search within a subtree
el.select(".x") / el.select_one(".x")
el.find_next_sibling("p") / el.find_previous_sibling("p")
el.decompose() / el.extract()                 # remove from the tree

New helpers & everyday conveniences

client = Client()

# generic method + query params + basic auth
client.request("POST", url, json={"a": 1})
client.get(url, params={"page": 2, "q": "hello world"})
client.get(url, auth=("user", "pass"))

# automatic retries with exponential backoff (retries 429/5xx)
client = Client(max_retries=3, retry_backoff=0.5, retry_codes=(429, 500, 502, 503, 504))

# concurrent requests (bounded thread pool)
results = client.map([url1, url2, url3], max_workers=8)

# cookies
client.cookies(url)          # -> dict of name -> value
client.clear_cookies()

# response helpers
resp.raise_for_status()
resp.save("body.html")
for chunk in resp.iter_content(65536): ...
for line in resp.iter_lines(): ...
resp.cookies                 # cookies for this response's url

Advanced HTTP

Redirects

# redirects are NOT followed by default (the 3xx response is returned as-is)
client = Client()
client.get("https://example.com/302")   # -> status_code 302

# follow up to N redirects
client = Client(max_redirects=5)
client.get("https://example.com/302")   # -> status_code 200 (final url)

HTTP/3 (QUIC)

client = Client(http3=True)      # tries h3, falls back to h1/h2 cleanly

Download to disk

client = Client()
client.download("https://example.com/file.bin", "file.bin")       # buffered
client.stream_to_file("https://example.com/big.bin", "big.bin")   # streamed

HTTP trailers

r = client.get("https://example.com")
print(r.trailers)      # trailing headers (dict)

Custom profile from a JSON file

# profile.json -> {"user_agent": "...", "platform": "Win32", "accept_language": "en-US,en;q=0.9", ...}
client = Client(profile="/path/to/profile.json")

Storage persistence

client = Client(storage_file="store.json")   # localStorage survives restarts/sessions

Diagnostics / debugging

client = Client(verbose=True)    # print go-side logs to stderr
client = Client(solution=True)   # attach live DNS/TCP/TLS diagnostics to any error

Why lithium (comparison)

Capability lithium primp tls_client curl_cffi
HTTP core
TLS (JA3) impersonation
Coherent multi-layer identity (UA↔headers↔navigator↔GPU↔screen) partial partial partial
Async client
HTTP/2 fingerprint (Chrome SETTINGS) partial
HTTP/3 (QUIC) opt-in
Streaming downloads
Proxy rotation / mTLS / CA
Custom CA + client cert
Geo / locale auto-detection
Browser runtime (the big differentiator)
JavaScript execution engine
Full DOM parsing + query (query/query_all/getElementById)
Selenium-style interaction (click/fill/type/select/wait_for)
Element events (addEventListener/dispatchEvent)
In-page fetch()/XHR → real HTTP
localStorage/sessionStorage (+ optional persistence)
IndexedDB
WebSocket (real connection)
WebCrypto (getRandomValues/randomUUID/subtle.digest)
MutationObserver
History/location API
Virtual Canvas 2D + WebGL
AudioContext / OfflineAudioContext
matchMedia / getComputedStyle / performance / permissions / getBattery
Media discovery + download (page.media)
Screenshots
Network inspection (page.network.requests)
Zero-config browser identity (no libs/browser install)
Per-request custom fingerprint profiles partial partial partial
Live diagnostics on errors (solution=True)

High stealth: Sannysoft proof

The runtime drives the official bot.sannysoft.com page and reads its own detection table, field by field. Every bot-detection field passes — 10 / 10 = 100%.

import json, re
from lithium import Client

with Client(execute_scripts=False, browser="chrome", platform="windows") as client:
    page = client.get("https://bot.sannysoft.com/")
    html = page.text

    # the site's own detection needs lodash (it uses _.has on navigator.webdriver)
    page.execute_script(
        client.get("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js", timeout=15).text
    )

    # run the site's OWN detection script (fills the real result table)
    for s in re.findall(r'<script(?![^>]*src)[^>]*>(.*?)</script>', html, re.S):
        if "runBotDetection" in s:
            try:
                page.execute_script(s)   # the site's real detection code
            except Exception:
                pass                     # harmless canvas-hash noise; table already filled
            break

    # read EVERY detection field the site marks pass/fail
    fields = json.loads(page.execute_script(r"""
      var out = [];
      function ok(el){ var c=' '+(el.className||'')+' '; return c.indexOf(' passed ')!==-1; }
      document.querySelectorAll('.result').forEach(function(el){
        out.push({field:el.id.replace('-result',''), ok:ok(el), value:(el.textContent||'').trim().slice(0,28)});
      });
      ['webgl-vendor','webgl-renderer'].forEach(function(id){
        var el=document.getElementById(id);
        out.push({field:id, ok:ok(el), value:(el.textContent||'').trim().slice(0,28)});
      });
      JSON.stringify(out);
    """))

    passed = sum(1 for f in fields if f['ok']); total = len(fields)
    for f in fields:
        print(f"  [{'PASS' if f['ok'] else 'FAIL'}] {f['field']:>18}  {f['value']}")
    pct = 100.0 * passed / total
    verdict = "PERFECT 100%" if pct >= 100 else ("90+ (high stealth)" if pct >= 90 else "below 90")
    print(f"SANNYSOFT: {passed}/{total} = {pct:.1f}% -> {verdict}")

Output:

  [PASS]         user-agent  Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/...
  [PASS]          webdriver  missing (passed)
  [PASS] advanced-webdriver  passed
  [PASS]             chrome  present (passed)
  [PASS]        permissions  prompt
  [PASS]     plugins-length  5
  [PASS]       plugins-type  passed
  [PASS]          languages  en-US,en
  [PASS]       webgl-vendor  Google Inc. (NVIDIA)
  [PASS]     webgl-renderer  ANGLE (NVIDIA, NVIDIA GeForce ...)
SANNYSOFT: 10/10 = 100.0% -> PERFECT 100%

Full honesty about the site's other cells: Sannysoft also has an advanced #fp2 table (21 fingerprint tests) filled by the site's async fpCollect.generateFingerprint(), which doesn't resolve inside a scripted runtime yet — so it can't be driven from a script. Its broken-image and canvas# cells are display cells (they need real Image.onerror and canvas rasterisation), not pass/fail bot checks. The 10 fields above are the core stealth/bot-detection checks, and all pass.

Proof of undetected: the default Client() also speaks a Chrome JA3 (e.g. 771,4865-4866-4867-49195-49199-...) verified against https://tls.peet.ws/api/all, and the JA3 version matches the UA version.


Every feature at a glance

HTTP

  • get / post / put / patch / delete / head / options
  • request(method, url, ...) generic method + query params=
  • Bodies: plain bytes/str, JSON (json=), urlencoded (data=dict), multipart (files=)
  • Cookies persisted per session automatically + client.cookies() / clear_cookies()
  • Redirect policy (max_redirects)
  • Basic auth (auth=(user, pass))
  • Retries with backoff (max_retries, retry_backoff, retry_codes)
  • Concurrent requests (client.map(urls, max_workers=))
  • Rotating proxy pools (proxies=[...])
  • Connection pooling tuning (max_idle_conns*)
  • Streaming downloads (stream_to_file)
  • Custom CA + mTLS (ca_cert / client_cert / client_key)
  • Geo / locale auto-detect (auto_location + friends, custom geo_url)
  • Network inspection (client.network, page.network.requests)
  • Async client (AsyncClient)
  • raise_for_status(), save(), iter_content(), iter_lines()
  • HTTP trailers (resp.trailers)

Browser runtime

  • DOM: page.dom.query / query_all, attributes, text, html, events
  • Interaction: fill, type, click, submit, select_option, check, uncheck, clear, wait_for, goto
  • JavaScript: page.execute_script, auto-run inline scripts
  • In-page fetch() / XMLHttpRequest → real HTTP
  • Storage: localStorage, sessionStorage (optionally persisted)
  • APIs: WebCrypto, MutationObserver, History/location, WebSocket, IndexedDB, matchMedia, getComputedStyle, performance, permissions, getBattery
  • Virtual graphics: Canvas 2D, WebGL, AudioContext
  • Screenshots (page.screenshot)
  • Media discovery + download (page.media, download_media)
  • BeautifulSoup-style DOM (find/find_all/select, name/string/get_text, parent/children/siblings/descendants, next_sibling/previous_sibling)
  • Coherent fingerprints (UA ↔ JA3 ↔ navigator ↔ GPU ↔ screen)

API reference — unique functions

Client

  • client.get/post/put/patch/delete/head/options(...)
  • client.download(url, path) — fetch and write to disk
  • client.stream_to_file(url, path) — stream response straight to disk
  • client.networkNetworkLog of every request (iterable, .requests, .responses)
  • client.close() / context manager

Response (a.k.a. the page)

  • status_code, headers, trailers, url, elapsed_ms, ok, text, json(), content
  • html (full live-DOM HTML), title (the page <title>)
  • timezone, currency, country_code (when auto-location on)
  • dom → query/query_all/wait_for
  • execute_script(js)
  • fill, type, click, submit, select_option, check, uncheck, clear
  • wait_for(selector, timeout=), goto(url) (both update the page in place)
  • screenshot(path=None, width=None, height=None, scale=2.0) → real CSS render, writes/returns PNG
  • networkNetworkLog
  • media → discovered <video>/<audio>/<img> sources
  • download(url, path), download_media(dir, kinds=None)
  • stream(chunk_size=) — chunked iterator

Element

  • text, html, get_attr(name, default=None), set_attr(name, value), remove()
  • add_event_listener(event, cb), dispatch_event(event, detail=None)
  • click(), fill(v), type(text, delay=), select_option(v), clear(), check(), uncheck(), submit()

AsyncClient

  • await versions of get/post/put/patch/delete/head/options/stream_to_file/download, plus network.

Made by Blaze, available on discord & github

feel free to contact me regarding any issues, suggestions or queries

License & note

lithium-web is under active development. The name, PyPI, and trademark availability should be confirmed before any public release, since similarly-named projects already exist.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

lithium_web-1.0b2.post1-py3-none-any.whl (37.8 MB view details)

Uploaded Python 3

File details

Details for the file lithium_web-1.0b2.post1-py3-none-any.whl.

File metadata

File hashes

Hashes for lithium_web-1.0b2.post1-py3-none-any.whl
Algorithm Hash digest
SHA256 0f277070d938e5aa3598ae3eb7a570c9a7a83adb02b6ceba831408b4dac7c147
MD5 71399da02a977db7f47b38d0430cf3ef
BLAKE2b-256 bd3b81be657b45a341184fcd0faa3daf99ff9d78ffdedac0c1e046565fc73369

See more details on using hashes here.

Release history Release notifications | RSS feed

1.0

1 file

This release

1.0b2.post1 This release

1 file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page