lithium-web 1.0b1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lithium_web-1.0b1/LICENSE +22 -0
- lithium_web-1.0b1/MANIFEST.in +4 -0
- lithium_web-1.0b1/PKG-INFO +418 -0
- lithium_web-1.0b1/README.md +397 -0
- lithium_web-1.0b1/lithium/__init__.py +8 -0
- lithium_web-1.0b1/lithium/bin/.lithium/fingerprints/canvas-noise-a.json +25 -0
- lithium_web-1.0b1/lithium/bin/.lithium/fingerprints/canvas-noise-b.json +26 -0
- lithium_web-1.0b1/lithium/bin/.lithium/fingerprints/conc-tab-924755-0.json +26 -0
- lithium_web-1.0b1/lithium/bin/.lithium/fingerprints/conc-tab-924755-1.json +25 -0
- lithium_web-1.0b1/lithium/bin/.lithium/fingerprints/conc-tab-924755-2.json +26 -0
- lithium_web-1.0b1/lithium/bin/.lithium/fingerprints/conc-tab-924755-3.json +25 -0
- lithium_web-1.0b1/lithium/bin/.lithium/fingerprints/one-off-override.json +26 -0
- lithium_web-1.0b1/lithium/bin/.lithium/fingerprints/persist-check-924755.json +26 -0
- lithium_web-1.0b1/lithium/bin/.lithium/fingerprints/separation-924755.json +26 -0
- lithium_web-1.0b1/lithium/bin/lithium-runtime-darwin-amd64 +0 -0
- lithium_web-1.0b1/lithium/bin/lithium-runtime-darwin-arm64 +0 -0
- lithium_web-1.0b1/lithium/bin/lithium-runtime-linux-amd64 +0 -0
- lithium_web-1.0b1/lithium/bin/lithium-runtime-linux-arm64 +0 -0
- lithium_web-1.0b1/lithium/bin/lithium-runtime-windows-amd64.exe +0 -0
- lithium_web-1.0b1/lithium/client.py +380 -0
- lithium_web-1.0b1/lithium/diagnostics.py +214 -0
- lithium_web-1.0b1/lithium/dom.py +38 -0
- lithium_web-1.0b1/lithium/element.py +116 -0
- lithium_web-1.0b1/lithium/response.py +237 -0
- lithium_web-1.0b1/lithium/session.py +13 -0
- lithium_web-1.0b1/lithium/transport/__init__.py +3 -0
- lithium_web-1.0b1/lithium/transport/connection.py +74 -0
- lithium_web-1.0b1/lithium/transport/process.py +112 -0
- lithium_web-1.0b1/lithium/transport/protocol.py +33 -0
- lithium_web-1.0b1/lithium/utils/__init__.py +3 -0
- lithium_web-1.0b1/lithium/utils/errors.py +13 -0
- lithium_web-1.0b1/lithium_web.egg-info/PKG-INFO +418 -0
- lithium_web-1.0b1/lithium_web.egg-info/SOURCES.txt +36 -0
- lithium_web-1.0b1/lithium_web.egg-info/dependency_links.txt +1 -0
- lithium_web-1.0b1/lithium_web.egg-info/requires.txt +1 -0
- lithium_web-1.0b1/lithium_web.egg-info/top_level.txt +1 -0
- lithium_web-1.0b1/pyproject.toml +53 -0
- lithium_web-1.0b1/setup.cfg +4 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OfficialDex
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lithium-web
|
|
3
|
+
Version: 1.0b1
|
|
4
|
+
Summary: An HTTP-first, stealth web runtime for Python — the simplicity of requests + the interaction model of Selenium.
|
|
5
|
+
Author: lithium-web contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/OfficialDex/lithium-web
|
|
8
|
+
Project-URL: Documentation, https://github.com/OfficialDex/lithium-web#readme
|
|
9
|
+
Keywords: http,http-client,web-scraping,undetected,browser-runtime,requests,selenium,fingerprint,curl,primp
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Go
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: msgpack>=1.0.0
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# Lithium
|
|
23
|
+
# Version – 1.0b1
|
|
24
|
+
|
|
25
|
+
**An HTTP-first, high stealth web runtime for Python.**
|
|
26
|
+
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.
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from lithium import Client
|
|
30
|
+
|
|
31
|
+
client = Client() # zero-config, undetected out of the box
|
|
32
|
+
page = client.get("https://example.com/login")
|
|
33
|
+
|
|
34
|
+
page.fill("#username", "alice")
|
|
35
|
+
page.fill("#password", "secret")
|
|
36
|
+
page.click("button[type=submit]")
|
|
37
|
+
page.wait_for("#dashboard")
|
|
38
|
+
|
|
39
|
+
print(page.text)
|
|
40
|
+
page.screenshot("dashboard.png")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
> Fun fact #1: the name comes from lithium — the lightest solid element — because the whole runtime binary is only ~19 MB.
|
|
44
|
+
> 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.
|
|
45
|
+
> 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.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install lithium-web
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
That's it.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Quickstart — major features, individually
|
|
60
|
+
|
|
61
|
+
### Requests-style HTTP
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from lithium import Client
|
|
65
|
+
|
|
66
|
+
client = Client()
|
|
67
|
+
r = client.get("https://api.example.com/users?limit=10")
|
|
68
|
+
print(r.status_code) # 200
|
|
69
|
+
print(r.ok) # True
|
|
70
|
+
print(r.json()) # parsed body
|
|
71
|
+
|
|
72
|
+
# POST with JSON
|
|
73
|
+
r = client.post("https://api.example.com/users", json={"name": "Ada"})
|
|
74
|
+
|
|
75
|
+
# POST with a form body (auto urlencoded)
|
|
76
|
+
r = client.post("https://httpbin.org/post", data={"q": "lithium"})
|
|
77
|
+
|
|
78
|
+
# POST multipart upload
|
|
79
|
+
r = client.post("https://httpbin.org/post",
|
|
80
|
+
data={"note": "hi"},
|
|
81
|
+
files={"file": ("report.pdf", b"%PDF-1.4 ...")})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Selenium-style page interaction
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from lithium import Client
|
|
88
|
+
|
|
89
|
+
page = client.get("https://example.com/login")
|
|
90
|
+
page.fill("#username", "alice")
|
|
91
|
+
page.fill("#password", "secret")
|
|
92
|
+
page.click("button[type=submit]") # navigates; `page` updates in place
|
|
93
|
+
page.wait_for("#dashboard") # waits for the element to appear
|
|
94
|
+
print(page.text) # text of the current page
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Execute JavaScript
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
page = client.get("https://example.com")
|
|
101
|
+
page.execute_script("document.body.dataset.loaded = 'true'")
|
|
102
|
+
print(page.dom.query("body").get_attr("data-loaded")) # 'true'
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Query the DOM
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
page = client.get("https://example.com/products")
|
|
109
|
+
items = page.dom.query_all(".product")
|
|
110
|
+
for el in items:
|
|
111
|
+
print(el.text, el.get_attr("data-price"))
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Screenshots
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
page = client.get("https://example.com")
|
|
118
|
+
page.screenshot("page.png") # writes a PNG, returns the path
|
|
119
|
+
png_bytes = page.screenshot() # or get raw bytes
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Async client
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
import asyncio
|
|
126
|
+
from lithium import AsyncClient
|
|
127
|
+
|
|
128
|
+
async def main():
|
|
129
|
+
async with AsyncClient() as client:
|
|
130
|
+
r = await client.get("https://example.com")
|
|
131
|
+
print(r.status_code)
|
|
132
|
+
|
|
133
|
+
asyncio.run(main())
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Streaming downloads
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
client = Client()
|
|
140
|
+
client.stream_to_file("https://example.com/bigfile.bin", "bigfile.bin")
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Proxies & rotating proxies
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
# single proxy
|
|
147
|
+
client = Client(proxy="socks5://user:pass@host:1080")
|
|
148
|
+
|
|
149
|
+
# rotating proxy pool (round-robin per request)
|
|
150
|
+
client = Client(proxies=[
|
|
151
|
+
"socks5://u:p@proxy1:1080",
|
|
152
|
+
"socks5://u:p@proxy2:1080",
|
|
153
|
+
])
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Custom CA & mutual TLS
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
client = Client(
|
|
160
|
+
ca_cert="/path/to/ca.pem",
|
|
161
|
+
client_cert="/path/to/client.crt",
|
|
162
|
+
client_key="/path/to/client.key",
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Geo / locale auto-detection
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
client = Client(auto_location=True) # timezone + currency + country
|
|
170
|
+
r = client.get("https://example.com")
|
|
171
|
+
print(r.timezone, r.currency, r.country_code)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Network inspection
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
page = client.get("https://example.com")
|
|
178
|
+
for req in page.network.requests: # every HTTP + in-page fetch/XHR
|
|
179
|
+
print(req.method, req.status, req.url)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Custom browser profile / fingerprint
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
# pick a coherent identity (UA + JA3 + navigator + GPU all agree)
|
|
186
|
+
client = Client(platform="windows", browser="chrome", fingerprint="my-identity")
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## One script that uses everything
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
import asyncio, json
|
|
195
|
+
from lithium import Client, AsyncClient
|
|
196
|
+
|
|
197
|
+
# 1. Requests-style client, undetected by default
|
|
198
|
+
client = Client(proxies=["socks5://u:p@h1:1080", "socks5://u:p@h2:1080"])
|
|
199
|
+
client.get("https://example.com/set-session")
|
|
200
|
+
|
|
201
|
+
# 2. A page we interact with, like Selenium
|
|
202
|
+
page = client.get("https://example.com/login")
|
|
203
|
+
page.fill("#username", "alice")
|
|
204
|
+
page.fill("#password", "secret")
|
|
205
|
+
page.check("#terms")
|
|
206
|
+
page.click("button[type=submit]")
|
|
207
|
+
page.wait_for("#dashboard")
|
|
208
|
+
print("landed:", page.status_code)
|
|
209
|
+
|
|
210
|
+
# 3. DOM + JS
|
|
211
|
+
rows = page.dom.query_all("table tbody tr")
|
|
212
|
+
page.execute_script("document.title = 'hello from js'")
|
|
213
|
+
|
|
214
|
+
# 4. Run JS that does real network (in-page fetch)
|
|
215
|
+
page.execute_script(
|
|
216
|
+
"fetch('/api/data').then(r=>r.json())"
|
|
217
|
+
".then(d=>document.body.setAttribute('data-h', d.hello));"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# 5. Rich media + screenshot
|
|
221
|
+
print("media:", page.media) # discovered video/audio/img sources
|
|
222
|
+
page.screenshot("dashboard.png")
|
|
223
|
+
|
|
224
|
+
# 6. Inspect every request the session made (HTTP + JS-driven)
|
|
225
|
+
for req in page.network.requests:
|
|
226
|
+
print(" net:", req.method, req.status, req.url)
|
|
227
|
+
|
|
228
|
+
# 7. Large download, streamed to disk
|
|
229
|
+
client.stream_to_file("https://example.com/db.sqlite", "db.sqlite")
|
|
230
|
+
|
|
231
|
+
# 8. Plain HTTP / JSON when you don't need a page
|
|
232
|
+
r = client.post("https://example.com/api", json={"a": 1})
|
|
233
|
+
print(r.json())
|
|
234
|
+
|
|
235
|
+
# 9. Async when you need concurrency
|
|
236
|
+
async def main():
|
|
237
|
+
async with AsyncClient() as ac:
|
|
238
|
+
results = await asyncio.gather(*[ac.get("https://example.com") for _ in range(5)])
|
|
239
|
+
print([r.status_code for r in results])
|
|
240
|
+
asyncio.run(main())
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Config reference
|
|
246
|
+
|
|
247
|
+
All options work as **direct keyword args** to `Client(...)` or via a `Config`/JSON file.
|
|
248
|
+
|
|
249
|
+
| Option | Default | What it does |
|
|
250
|
+
|---|---|---|
|
|
251
|
+
| `profile` | `"chrome-windows"` | Coherent browser identity (UA + headers + JA3 + navigator). |
|
|
252
|
+
| `fingerprint` | `None` | Persistent identity name; same name = same profile across runs. |
|
|
253
|
+
| `platform` | `None` | Force `windows` / `macos` / `linux` / `android` / `ios`. |
|
|
254
|
+
| `browser` | `None` | Force `chrome` / `firefox` / `opera` / `brave`. |
|
|
255
|
+
| `proxy` | `None` | Single proxy URL (http/https/socks4/socks4a/socks5/socks5h). |
|
|
256
|
+
| `proxies` | `None` | List of proxy URLs, rotated round-robin per request. |
|
|
257
|
+
| `verify_ssl` | `True` | Verify server certificates. |
|
|
258
|
+
| `ca_cert`, `client_cert`, `client_key` | `None` | Custom CA bundle and client certificate (mTLS). |
|
|
259
|
+
| `trust_env` | `True` | Respect `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY` env vars. |
|
|
260
|
+
| `tls_spoof` | `None` | Explicitly enable/disable TLS (JA3) impersonation. |
|
|
261
|
+
| `http3` | `False` | Try HTTP/3 (QUIC) with graceful h1/h2 fallback. |
|
|
262
|
+
| `execute_scripts` | `False` | Auto-run inline `<script>` on page load. |
|
|
263
|
+
| `js_timeout` | `10.0` | Max seconds a JS call may run. |
|
|
264
|
+
| `timeout` | `30.0` | Default request timeout (seconds). |
|
|
265
|
+
| `max_redirects` | `0` | Redirect limit (0=Go default 10, `-1`=don't follow). |
|
|
266
|
+
| `max_idle_conns`, `max_idle_conns_per_host` | `100`, `10` | HTTP connection-pool sizing. |
|
|
267
|
+
| `viewport_width`, `viewport_height` | platform default | Screen/inner size (`screen`, `innerWidth`, `matchMedia`). |
|
|
268
|
+
| `device_scale_factor` | platform default | `devicePixelRatio` (mobile auto-sets to 2). |
|
|
269
|
+
| `screen_color_depth` | `24` | `screen.colorDepth` / `pixelDepth`. |
|
|
270
|
+
| `hardware_concurrency`, `device_memory` | from fingerprint | `navigator.hardwareConcurrency` / `.deviceMemory`. |
|
|
271
|
+
| `max_touch_points` | `0` | `navigator.maxTouchPoints`. |
|
|
272
|
+
| `do_not_track` | `None` | `navigator.doNotTrack`. |
|
|
273
|
+
| `audio_sample_rate` | `44100` | `AudioContext().sampleRate`. |
|
|
274
|
+
| `screenshot_width`, `screenshot_height` | `1280`, `800` | PNG size for `page.screenshot()`. |
|
|
275
|
+
| `virtual_gpu` | `True` | Enable virtual WebGL context. |
|
|
276
|
+
| `canvas_noise` | `True` | Make `toDataURL` vary per identity. |
|
|
277
|
+
| `auto_location`, `auto_timezone`, `auto_currency`, `auto_language` | `None` | Auto-detect locale from your IP (via `geo_url`). |
|
|
278
|
+
| `geo_url` | ip-api.com | Custom geo lookup endpoint. |
|
|
279
|
+
| `storage_file` | `None` | Persist `localStorage` across runs/sessions. |
|
|
280
|
+
| `socket_timeout` | `None` | Seconds to wait for a reply from the runtime. |
|
|
281
|
+
| `shared` | `True` | Reuse one runtime process across clients. |
|
|
282
|
+
| `binary` | auto | Path to the runtime binary (override). |
|
|
283
|
+
| `verbose` | `False` | Print go-side logs to stderr. |
|
|
284
|
+
| `solution` | `False` | Attach live DNS/TCP/TLS diagnostics to errors. |
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Why lithium (comparison)
|
|
289
|
+
|
|
290
|
+
| Capability | **lithium** | primp | tls_client | curl_cffi |
|
|
291
|
+
|---|:---:|:---:|:---:|:---:|
|
|
292
|
+
| TLS (JA3) impersonation | ✅ | ✅ | ✅ | ✅ |
|
|
293
|
+
| Coherent multi-layer identity (UA↔headers↔navigator↔GPU) | ✅ | partial | partial | partial |
|
|
294
|
+
| **Selenium-style interaction** (`click`/`fill`/`wait_for`/`select`) | ✅ | ❌ | ❌ | ❌ |
|
|
295
|
+
| **Full DOM querying** (`query`/`query_all`) | ✅ | ❌ | ❌ | ❌ |
|
|
296
|
+
| **JavaScript execution** | ✅ | ❌ | ❌ | ❌ |
|
|
297
|
+
| **In-page `fetch()`/XHR → real HTTP** | ✅ | ❌ | ❌ | ❌ |
|
|
298
|
+
| **`localStorage`/`indexedDB`/`WebSocket`/`WebCrypto`** | ✅ | ❌ | ❌ | ❌ |
|
|
299
|
+
| **Screenshots** | ✅ | ❌ | ❌ | ❌ |
|
|
300
|
+
| **Network inspection** (`page.network.requests`) | ✅ | ❌ | ❌ | ❌ |
|
|
301
|
+
| **Zero-config browser identity** (no libs/browser install) | ✅ | ❌ | ❌ | ❌ |
|
|
302
|
+
| Async | ✅ | ✅ | ✅ | ✅ |
|
|
303
|
+
| HTTP/2 fingerprint (Chrome SETTINGS) | partial | ✅ | ✅ | ✅ |
|
|
304
|
+
| HTTP/3 | opt-in | ✅ | ✅ | ✅ |
|
|
305
|
+
| Streaming downloads | ✅ | ✅ | ✅ | ✅ |
|
|
306
|
+
| Proxy rotation / mTLS / CA | ✅ | ✅ | ✅ | ✅ |
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## High stealth: Sannysoft proof
|
|
311
|
+
|
|
312
|
+
The runtime drives the **official `bot.sannysoft.com`** page and reads its **own detection table**: **8 / 8 passed, 0 failed**. Run it yourself:
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
import json, re
|
|
316
|
+
from lithium import Client
|
|
317
|
+
|
|
318
|
+
with Client(execute_scripts=False, browser="chrome", platform="windows") as client:
|
|
319
|
+
page = client.get("https://bot.sannysoft.com/")
|
|
320
|
+
html = page.text
|
|
321
|
+
|
|
322
|
+
# the site's own detection needs lodash (it uses _.has on navigator.webdriver)
|
|
323
|
+
page.execute_script(
|
|
324
|
+
client.get("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js", timeout=15).text
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# run the site's OWN detection script (fills the real main result table)
|
|
328
|
+
for s in re.findall(r'<script(?![^>]*src)[^>]*>(.*?)</script>', html, re.S):
|
|
329
|
+
if "runBotDetection" in s:
|
|
330
|
+
try:
|
|
331
|
+
page.execute_script(s) # the site's real detection code
|
|
332
|
+
except Exception:
|
|
333
|
+
pass # harmless canvas-hash noise; table already filled
|
|
334
|
+
break
|
|
335
|
+
|
|
336
|
+
counts = json.loads(page.execute_script(
|
|
337
|
+
"JSON.stringify({passed: document.querySelectorAll('.result.passed').length,"
|
|
338
|
+
" failed: document.querySelectorAll('.result.failed').length})"))
|
|
339
|
+
print(f"OFFICIAL sannysoft: {counts['passed']} passed, {counts['failed']} failed")
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Output: `OFFICIAL sannysoft: 8 passed, 0 failed`.
|
|
343
|
+
|
|
344
|
+
> 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.
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## Every feature at a glance
|
|
349
|
+
|
|
350
|
+
**HTTP**
|
|
351
|
+
- `get` / `post` / `put` / `patch` / `delete` / `head` / `options`
|
|
352
|
+
- Bodies: plain bytes/str, JSON (`json=`), urlencoded (`data=dict`), multipart (`files=`)
|
|
353
|
+
- Cookies persisted per session automatically
|
|
354
|
+
- Redirect policy (`max_redirects`)
|
|
355
|
+
- Rotating proxy pools (`proxies=[...]`)
|
|
356
|
+
- Connection pooling tuning (`max_idle_conns*`)
|
|
357
|
+
- Streaming downloads (`stream_to_file`)
|
|
358
|
+
- Custom CA + mTLS (`ca_cert` / `client_cert` / `client_key`)
|
|
359
|
+
- Geo / locale auto-detect (`auto_location` + friends, custom `geo_url`)
|
|
360
|
+
- Network inspection (`client.network`, `page.network.requests`)
|
|
361
|
+
- Async client (`AsyncClient`)
|
|
362
|
+
- HTTP trailers (`resp.trailers`)
|
|
363
|
+
|
|
364
|
+
**Browser runtime**
|
|
365
|
+
- DOM: `page.dom.query` / `query_all`, attributes, text, html, events
|
|
366
|
+
- Interaction: `fill`, `type`, `click`, `submit`, `select_option`, `check`, `uncheck`, `clear`, `wait_for`, `goto`
|
|
367
|
+
- JavaScript: `page.execute_script`, auto-run inline scripts
|
|
368
|
+
- In-page `fetch()` / `XMLHttpRequest` → real HTTP
|
|
369
|
+
- Storage: `localStorage`, `sessionStorage` (optionally persisted)
|
|
370
|
+
- APIs: `WebCrypto`, `MutationObserver`, `History`/`location`, `WebSocket`, `IndexedDB`, `matchMedia`, `getComputedStyle`, `performance`, `permissions`, `getBattery`
|
|
371
|
+
- Virtual graphics: Canvas 2D, WebGL, `AudioContext`
|
|
372
|
+
- Screenshots (`page.screenshot`)
|
|
373
|
+
- Media discovery + download (`page.media`, `download_media`)
|
|
374
|
+
- Coherent fingerprints (UA ↔ JA3 ↔ `navigator` ↔ GPU ↔ screen)
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## API reference — unique functions
|
|
379
|
+
|
|
380
|
+
**`Client`**
|
|
381
|
+
- `client.get/post/put/patch/delete/head/options(...)`
|
|
382
|
+
- `client.download(url, path)` — fetch and write to disk
|
|
383
|
+
- `client.stream_to_file(url, path)` — stream response straight to disk
|
|
384
|
+
- `client.network` — `NetworkLog` of every request (iterable, `.requests`, `.responses`)
|
|
385
|
+
- `client.close()` / context manager
|
|
386
|
+
|
|
387
|
+
**`Response` (a.k.a. the page)**
|
|
388
|
+
- `status_code`, `headers`, `trailers`, `url`, `elapsed_ms`, `ok`, `text`, `json()`, `content`
|
|
389
|
+
- `timezone`, `currency`, `country_code` (when auto-location on)
|
|
390
|
+
- `dom` → query/query_all/wait_for
|
|
391
|
+
- `execute_script(js)`
|
|
392
|
+
- `fill`, `type`, `click`, `submit`, `select_option`, `check`, `uncheck`, `clear`
|
|
393
|
+
- `wait_for(selector, timeout=)`, `goto(url)` (both update the page in place)
|
|
394
|
+
- `screenshot(path=None)` → writes/returns PNG
|
|
395
|
+
- `network` → `NetworkLog`
|
|
396
|
+
- `media` → discovered `<video>/<audio>/<img>` sources
|
|
397
|
+
- `download(url, path)`, `download_media(dir, kinds=None)`
|
|
398
|
+
- `stream(chunk_size=)` — chunked iterator
|
|
399
|
+
|
|
400
|
+
**`Element`**
|
|
401
|
+
- `text`, `html`, `get_attr(name, default=None)`, `set_attr(name, value)`, `remove()`
|
|
402
|
+
- `add_event_listener(event, cb)`, `dispatch_event(event, detail=None)`
|
|
403
|
+
- `click()`, `fill(v)`, `type(text, delay=)`, `select_option(v)`, `clear()`, `check()`, `uncheck()`, `submit()`
|
|
404
|
+
|
|
405
|
+
**`AsyncClient`**
|
|
406
|
+
- `await` versions of `get/post/put/patch/delete/head/options/stream_to_file/download`, plus `network`.
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
# Made by Blaze, available on [discord](https://discord.com/users/1238444724386533417) & [github](https://github.com/OfficialDex)
|
|
411
|
+
feel free to contact me regarding any issues, suggestions or queries
|
|
412
|
+
|
|
413
|
+
## License & note
|
|
414
|
+
|
|
415
|
+
`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.
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
|