Metadata-Version: 2.4
Name: lithium-web
Version: 1.0b1
Summary: An HTTP-first, stealth web runtime for Python — the simplicity of requests + the interaction model of Selenium.
Author: lithium-web contributors
License-Expression: MIT
Project-URL: Homepage, https://github.com/OfficialDex/lithium-web
Project-URL: Documentation, https://github.com/OfficialDex/lithium-web#readme
Keywords: http,http-client,web-scraping,undetected,browser-runtime,requests,selenium,fingerprint,curl,primp
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Go
Classifier: Operating System :: OS Independent
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: msgpack>=1.0.0
Dynamic: license-file

# Lithium
# Version – 1.0b1

**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.

```python
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

```bash
pip install lithium-web
```

That's it.

---

## Quickstart — major features, individually

### Requests-style HTTP

```python
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

```python
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

```python
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

```python
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

```python
page = client.get("https://example.com")
page.screenshot("page.png")          # writes a PNG, returns the path
png_bytes = page.screenshot()        # or get raw bytes
```

### Async client

```python
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

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

### Proxies & rotating proxies

```python
# 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

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

### Geo / locale auto-detection

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

### Network inspection

```python
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

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

---

## One script that uses everything

```python
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=Go default 10, `-1`=don't follow). |
| `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`, `800` | PNG size 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. |

---

## Why lithium (comparison)

| Capability | **lithium** | primp | tls_client | curl_cffi |
|---|:---:|:---:|:---:|:---:|
| TLS (JA3) impersonation | ✅ | ✅ | ✅ | ✅ |
| Coherent multi-layer identity (UA↔headers↔navigator↔GPU) | ✅ | partial | partial | partial |
| **Selenium-style interaction** (`click`/`fill`/`wait_for`/`select`) | ✅ | ❌ | ❌ | ❌ |
| **Full DOM querying** (`query`/`query_all`) | ✅ | ❌ | ❌ | ❌ |
| **JavaScript execution** | ✅ | ❌ | ❌ | ❌ |
| **In-page `fetch()`/XHR → real HTTP** | ✅ | ❌ | ❌ | ❌ |
| **`localStorage`/`indexedDB`/`WebSocket`/`WebCrypto`** | ✅ | ❌ | ❌ | ❌ |
| **Screenshots** | ✅ | ❌ | ❌ | ❌ |
| **Network inspection** (`page.network.requests`) | ✅ | ❌ | ❌ | ❌ |
| **Zero-config browser identity** (no libs/browser install) | ✅ | ❌ | ❌ | ❌ |
| Async | ✅ | ✅ | ✅ | ✅ |
| HTTP/2 fingerprint (Chrome SETTINGS) | partial | ✅ | ✅ | ✅ |
| HTTP/3 | opt-in | ✅ | ✅ | ✅ |
| Streaming downloads | ✅ | ✅ | ✅ | ✅ |
| Proxy rotation / mTLS / CA | ✅ | ✅ | ✅ | ✅ |

---

## High stealth: Sannysoft proof

The runtime drives the **official `bot.sannysoft.com`** page and reads its **own detection table**: **8 / 8 passed, 0 failed**. Run it yourself:

```python
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 main 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

    counts = json.loads(page.execute_script(
        "JSON.stringify({passed: document.querySelectorAll('.result.passed').length,"
        " failed: document.querySelectorAll('.result.failed').length})"))
    print(f"OFFICIAL sannysoft: {counts['passed']} passed, {counts['failed']} failed")
```

Output: `OFFICIAL sannysoft: 8 passed, 0 failed`.

> 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`
- Bodies: plain bytes/str, JSON (`json=`), urlencoded (`data=dict`), multipart (`files=`)
- Cookies persisted per session automatically
- Redirect policy (`max_redirects`)
- 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`)
- 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`)
- 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.network` — `NetworkLog` 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`
- `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)` → writes/returns PNG
- `network` → `NetworkLog`
- `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](https://discord.com/users/1238444724386533417) & [github](https://github.com/OfficialDex)
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.



