lithium-web 1.0b1__py3-none-any.whl
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/__init__.py +8 -0
- lithium/bin/.lithium/fingerprints/canvas-noise-a.json +25 -0
- lithium/bin/.lithium/fingerprints/canvas-noise-b.json +26 -0
- lithium/bin/.lithium/fingerprints/conc-tab-924755-0.json +26 -0
- lithium/bin/.lithium/fingerprints/conc-tab-924755-1.json +25 -0
- lithium/bin/.lithium/fingerprints/conc-tab-924755-2.json +26 -0
- lithium/bin/.lithium/fingerprints/conc-tab-924755-3.json +25 -0
- lithium/bin/.lithium/fingerprints/one-off-override.json +26 -0
- lithium/bin/.lithium/fingerprints/persist-check-924755.json +26 -0
- lithium/bin/.lithium/fingerprints/separation-924755.json +26 -0
- lithium/bin/lithium-runtime-darwin-amd64 +0 -0
- lithium/bin/lithium-runtime-darwin-arm64 +0 -0
- lithium/bin/lithium-runtime-linux-amd64 +0 -0
- lithium/bin/lithium-runtime-linux-arm64 +0 -0
- lithium/bin/lithium-runtime-windows-amd64.exe +0 -0
- lithium/client.py +380 -0
- lithium/diagnostics.py +214 -0
- lithium/dom.py +38 -0
- lithium/element.py +116 -0
- lithium/response.py +237 -0
- lithium/session.py +13 -0
- lithium/transport/__init__.py +3 -0
- lithium/transport/connection.py +74 -0
- lithium/transport/process.py +112 -0
- lithium/transport/protocol.py +33 -0
- lithium/utils/__init__.py +3 -0
- lithium/utils/errors.py +13 -0
- lithium_web-1.0b1.dist-info/METADATA +418 -0
- lithium_web-1.0b1.dist-info/RECORD +32 -0
- lithium_web-1.0b1.dist-info/WHEEL +5 -0
- lithium_web-1.0b1.dist-info/licenses/LICENSE +22 -0
- lithium_web-1.0b1.dist-info/top_level.txt +1 -0
lithium/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .client import Client, Config, AsyncClient
|
|
2
|
+
from .response import Response, RequestInfo, NetworkLog
|
|
3
|
+
from .session import Session
|
|
4
|
+
from .dom import Dom
|
|
5
|
+
from .element import Element
|
|
6
|
+
|
|
7
|
+
__all__ = ["Client", "Config", "AsyncClient", "Response", "Session", "Dom", "Element", "RequestInfo", "NetworkLog"]
|
|
8
|
+
__version__ = "1.0b1"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "canvas-noise-a",
|
|
3
|
+
"platform": "windows",
|
|
4
|
+
"browser": "chrome",
|
|
5
|
+
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
|
|
6
|
+
"sec_ch_ua": "\"Not?A_Brand\";v=\"99\", \"Chromium\";v=\"133\", \"Google Chrome\";v=\"133\"",
|
|
7
|
+
"sec_ch_ua_platform": "\"Windows\"",
|
|
8
|
+
"sec_ch_ua_mobile": "?0",
|
|
9
|
+
"navigator_platform": "Win32",
|
|
10
|
+
"vendor": "Google Inc.",
|
|
11
|
+
"accept_language": "en-GB, en",
|
|
12
|
+
"languages": [
|
|
13
|
+
"en-GB",
|
|
14
|
+
"en"
|
|
15
|
+
],
|
|
16
|
+
"hardware_concurrency": 4,
|
|
17
|
+
"device_memory": 4,
|
|
18
|
+
"has_device_memory": true,
|
|
19
|
+
"has_chrome_object": true,
|
|
20
|
+
"is_brave": false,
|
|
21
|
+
"gpu_vendor": "Google Inc. (AMD)",
|
|
22
|
+
"gpu_renderer": "ANGLE (AMD, AMD Radeon RX 6700 XT Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
|
23
|
+
"browser_version": "133",
|
|
24
|
+
"created_at": "2026-09-01"
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "canvas-noise-b",
|
|
3
|
+
"platform": "windows",
|
|
4
|
+
"browser": "brave",
|
|
5
|
+
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
|
|
6
|
+
"sec_ch_ua": "\"Chromium\";v=\"133\", \"Brave\";v=\"133\", \"Not.A/Brand\";v=\"99\"",
|
|
7
|
+
"sec_ch_ua_platform": "\"Windows\"",
|
|
8
|
+
"sec_ch_ua_mobile": "?0",
|
|
9
|
+
"navigator_platform": "Win32",
|
|
10
|
+
"vendor": "Google Inc.",
|
|
11
|
+
"accept_language": "fr-FR, fr, en",
|
|
12
|
+
"languages": [
|
|
13
|
+
"fr-FR",
|
|
14
|
+
"fr",
|
|
15
|
+
"en"
|
|
16
|
+
],
|
|
17
|
+
"hardware_concurrency": 8,
|
|
18
|
+
"device_memory": 8,
|
|
19
|
+
"has_device_memory": true,
|
|
20
|
+
"has_chrome_object": true,
|
|
21
|
+
"is_brave": true,
|
|
22
|
+
"gpu_vendor": "Google Inc. (AMD)",
|
|
23
|
+
"gpu_renderer": "ANGLE (AMD, AMD Radeon RX 580 Series Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
|
24
|
+
"browser_version": "133",
|
|
25
|
+
"created_at": "2026-09-01"
|
|
26
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "conc-tab-924755-0",
|
|
3
|
+
"platform": "windows",
|
|
4
|
+
"browser": "brave",
|
|
5
|
+
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
|
|
6
|
+
"sec_ch_ua": "\"Chromium\";v=\"133\", \"Brave\";v=\"133\", \"Not.A/Brand\";v=\"99\"",
|
|
7
|
+
"sec_ch_ua_platform": "\"Windows\"",
|
|
8
|
+
"sec_ch_ua_mobile": "?0",
|
|
9
|
+
"navigator_platform": "Win32",
|
|
10
|
+
"vendor": "Google Inc.",
|
|
11
|
+
"accept_language": "fr-FR, fr, en",
|
|
12
|
+
"languages": [
|
|
13
|
+
"fr-FR",
|
|
14
|
+
"fr",
|
|
15
|
+
"en"
|
|
16
|
+
],
|
|
17
|
+
"hardware_concurrency": 8,
|
|
18
|
+
"device_memory": 16,
|
|
19
|
+
"has_device_memory": true,
|
|
20
|
+
"has_chrome_object": true,
|
|
21
|
+
"is_brave": true,
|
|
22
|
+
"gpu_vendor": "Google Inc. (Intel)",
|
|
23
|
+
"gpu_renderer": "ANGLE (Intel, Intel(R) UHD Graphics 630 Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
|
24
|
+
"browser_version": "133",
|
|
25
|
+
"created_at": "2026-09-01"
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "conc-tab-924755-1",
|
|
3
|
+
"platform": "windows",
|
|
4
|
+
"browser": "chrome",
|
|
5
|
+
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
|
|
6
|
+
"sec_ch_ua": "\"Not?A_Brand\";v=\"99\", \"Chromium\";v=\"133\", \"Google Chrome\";v=\"133\"",
|
|
7
|
+
"sec_ch_ua_platform": "\"Windows\"",
|
|
8
|
+
"sec_ch_ua_mobile": "?0",
|
|
9
|
+
"navigator_platform": "Win32",
|
|
10
|
+
"vendor": "Google Inc.",
|
|
11
|
+
"accept_language": "en-US, en",
|
|
12
|
+
"languages": [
|
|
13
|
+
"en-US",
|
|
14
|
+
"en"
|
|
15
|
+
],
|
|
16
|
+
"hardware_concurrency": 8,
|
|
17
|
+
"device_memory": 8,
|
|
18
|
+
"has_device_memory": true,
|
|
19
|
+
"has_chrome_object": true,
|
|
20
|
+
"is_brave": false,
|
|
21
|
+
"gpu_vendor": "Google Inc. (AMD)",
|
|
22
|
+
"gpu_renderer": "ANGLE (AMD, AMD Radeon RX 6700 XT Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
|
23
|
+
"browser_version": "133",
|
|
24
|
+
"created_at": "2026-09-01"
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "conc-tab-924755-2",
|
|
3
|
+
"platform": "windows",
|
|
4
|
+
"browser": "firefox",
|
|
5
|
+
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:153.0.4) Gecko/20100101 Firefox/153.0.4",
|
|
6
|
+
"sec_ch_ua": "",
|
|
7
|
+
"sec_ch_ua_platform": "",
|
|
8
|
+
"sec_ch_ua_mobile": "",
|
|
9
|
+
"navigator_platform": "Win32",
|
|
10
|
+
"vendor": "",
|
|
11
|
+
"accept_language": "de-DE, de, en",
|
|
12
|
+
"languages": [
|
|
13
|
+
"de-DE",
|
|
14
|
+
"de",
|
|
15
|
+
"en"
|
|
16
|
+
],
|
|
17
|
+
"hardware_concurrency": 4,
|
|
18
|
+
"device_memory": 8,
|
|
19
|
+
"has_device_memory": true,
|
|
20
|
+
"has_chrome_object": false,
|
|
21
|
+
"is_brave": false,
|
|
22
|
+
"gpu_vendor": "NVIDIA Corporation",
|
|
23
|
+
"gpu_renderer": "NVIDIA GeForce RTX 3060/PCIe/SSE2",
|
|
24
|
+
"browser_version": "153.0.4",
|
|
25
|
+
"created_at": "2026-09-01"
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "conc-tab-924755-3",
|
|
3
|
+
"platform": "windows",
|
|
4
|
+
"browser": "chrome",
|
|
5
|
+
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
|
|
6
|
+
"sec_ch_ua": "\"Not?A_Brand\";v=\"99\", \"Chromium\";v=\"133\", \"Google Chrome\";v=\"133\"",
|
|
7
|
+
"sec_ch_ua_platform": "\"Windows\"",
|
|
8
|
+
"sec_ch_ua_mobile": "?0",
|
|
9
|
+
"navigator_platform": "Win32",
|
|
10
|
+
"vendor": "Google Inc.",
|
|
11
|
+
"accept_language": "en-US, en",
|
|
12
|
+
"languages": [
|
|
13
|
+
"en-US",
|
|
14
|
+
"en"
|
|
15
|
+
],
|
|
16
|
+
"hardware_concurrency": 8,
|
|
17
|
+
"device_memory": 8,
|
|
18
|
+
"has_device_memory": true,
|
|
19
|
+
"has_chrome_object": true,
|
|
20
|
+
"is_brave": false,
|
|
21
|
+
"gpu_vendor": "Google Inc. (Intel)",
|
|
22
|
+
"gpu_renderer": "ANGLE (Intel, Intel(R) UHD Graphics 630 Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
|
23
|
+
"browser_version": "133",
|
|
24
|
+
"created_at": "2026-09-01"
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "one-off-override",
|
|
3
|
+
"platform": "macos",
|
|
4
|
+
"browser": "firefox",
|
|
5
|
+
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:153.0.3) Gecko/20100101 Firefox/153.0.3",
|
|
6
|
+
"sec_ch_ua": "",
|
|
7
|
+
"sec_ch_ua_platform": "",
|
|
8
|
+
"sec_ch_ua_mobile": "",
|
|
9
|
+
"navigator_platform": "MacIntel",
|
|
10
|
+
"vendor": "",
|
|
11
|
+
"accept_language": "es-ES, es, en",
|
|
12
|
+
"languages": [
|
|
13
|
+
"es-ES",
|
|
14
|
+
"es",
|
|
15
|
+
"en"
|
|
16
|
+
],
|
|
17
|
+
"hardware_concurrency": 8,
|
|
18
|
+
"device_memory": 8,
|
|
19
|
+
"has_device_memory": true,
|
|
20
|
+
"has_chrome_object": false,
|
|
21
|
+
"is_brave": false,
|
|
22
|
+
"gpu_vendor": "Apple Inc.",
|
|
23
|
+
"gpu_renderer": "Apple M1 Pro",
|
|
24
|
+
"browser_version": "153.0.3",
|
|
25
|
+
"created_at": "2026-09-01"
|
|
26
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "persist-check-924755",
|
|
3
|
+
"platform": "linux",
|
|
4
|
+
"browser": "chrome",
|
|
5
|
+
"user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
|
|
6
|
+
"sec_ch_ua": "\"Not?A_Brand\";v=\"99\", \"Chromium\";v=\"133\", \"Google Chrome\";v=\"133\"",
|
|
7
|
+
"sec_ch_ua_platform": "\"Linux\"",
|
|
8
|
+
"sec_ch_ua_mobile": "?0",
|
|
9
|
+
"navigator_platform": "Linux x86_64",
|
|
10
|
+
"vendor": "Google Inc.",
|
|
11
|
+
"accept_language": "fr-FR, fr, en",
|
|
12
|
+
"languages": [
|
|
13
|
+
"fr-FR",
|
|
14
|
+
"fr",
|
|
15
|
+
"en"
|
|
16
|
+
],
|
|
17
|
+
"hardware_concurrency": 8,
|
|
18
|
+
"device_memory": 4,
|
|
19
|
+
"has_device_memory": true,
|
|
20
|
+
"has_chrome_object": true,
|
|
21
|
+
"is_brave": false,
|
|
22
|
+
"gpu_vendor": "Google Inc. (Intel Open Source Technology Center)",
|
|
23
|
+
"gpu_renderer": "ANGLE (Intel Open Source Technology Center, Mesa DRI Intel(R) HD Graphics 4600 (HSW GT2), OpenGL 4.5)",
|
|
24
|
+
"browser_version": "133",
|
|
25
|
+
"created_at": "2026-09-01"
|
|
26
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "separation-924755",
|
|
3
|
+
"platform": "linux",
|
|
4
|
+
"browser": "chrome",
|
|
5
|
+
"user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
6
|
+
"sec_ch_ua": "\"Not?A_Brand\";v=\"99\", \"Chromium\";v=\"131\", \"Google Chrome\";v=\"131\"",
|
|
7
|
+
"sec_ch_ua_platform": "\"Linux\"",
|
|
8
|
+
"sec_ch_ua_mobile": "?0",
|
|
9
|
+
"navigator_platform": "Linux x86_64",
|
|
10
|
+
"vendor": "Google Inc.",
|
|
11
|
+
"accept_language": "en-CA, en, fr",
|
|
12
|
+
"languages": [
|
|
13
|
+
"en-CA",
|
|
14
|
+
"en",
|
|
15
|
+
"fr"
|
|
16
|
+
],
|
|
17
|
+
"hardware_concurrency": 8,
|
|
18
|
+
"device_memory": 4,
|
|
19
|
+
"has_device_memory": true,
|
|
20
|
+
"has_chrome_object": true,
|
|
21
|
+
"is_brave": false,
|
|
22
|
+
"gpu_vendor": "Google Inc. (Intel Open Source Technology Center)",
|
|
23
|
+
"gpu_renderer": "ANGLE (Intel Open Source Technology Center, Mesa DRI Intel(R) HD Graphics 4600 (HSW GT2), OpenGL 4.5)",
|
|
24
|
+
"browser_version": "131",
|
|
25
|
+
"created_at": "2026-09-01"
|
|
26
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
lithium/client.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json as jsonlib
|
|
3
|
+
import uuid
|
|
4
|
+
from urllib.parse import urlencode
|
|
5
|
+
|
|
6
|
+
from .transport import connection
|
|
7
|
+
from .response import Response, NetworkLog
|
|
8
|
+
from .session import Session
|
|
9
|
+
from .utils import LithiumError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Config:
|
|
13
|
+
|
|
14
|
+
def __init__(self, file=None, profile="chrome-windows", fingerprint=None, platform=None, browser=None, proxy=None,
|
|
15
|
+
verify_ssl=True, trust_env=True, execute_scripts=False, js_timeout=10.0, timeout=30.0,
|
|
16
|
+
solution=False, shared=True, verbose=False, binary=None,
|
|
17
|
+
auto_location=None, auto_timezone=None, auto_currency=None, auto_language=None,
|
|
18
|
+
virtual_gpu=True, canvas_noise=True, tls_spoof=None, http3=False,
|
|
19
|
+
socket_timeout=None, storage_file=None,
|
|
20
|
+
viewport_width=None, viewport_height=None, device_scale_factor=None, screen_color_depth=None,
|
|
21
|
+
hardware_concurrency=None, device_memory=None, max_touch_points=None, do_not_track=None,
|
|
22
|
+
audio_sample_rate=None, screenshot_width=None, screenshot_height=None,
|
|
23
|
+
max_idle_conns=None, max_idle_conns_per_host=None, geo_url=None,
|
|
24
|
+
proxies=None, max_redirects=None, ca_cert=None, client_cert=None, client_key=None):
|
|
25
|
+
data = {}
|
|
26
|
+
if file:
|
|
27
|
+
with open(file) as f:
|
|
28
|
+
data = jsonlib.load(f)
|
|
29
|
+
|
|
30
|
+
self.profile = data.get("profile", profile)
|
|
31
|
+
self.fingerprint = data.get("fingerprint", fingerprint)
|
|
32
|
+
self.platform = data.get("platform", platform)
|
|
33
|
+
self.browser = data.get("browser", browser)
|
|
34
|
+
self.proxy = data.get("proxy", proxy)
|
|
35
|
+
self.verify_ssl = data.get("verify_ssl", verify_ssl)
|
|
36
|
+
self.trust_env = data.get("trust_env", trust_env)
|
|
37
|
+
self.execute_scripts = data.get("execute_scripts", execute_scripts)
|
|
38
|
+
self.js_timeout = data.get("js_timeout", js_timeout)
|
|
39
|
+
self.timeout = data.get("timeout", timeout)
|
|
40
|
+
self.solution = data.get("solution", solution)
|
|
41
|
+
self.shared = data.get("shared", shared)
|
|
42
|
+
self.verbose = data.get("verbose", verbose)
|
|
43
|
+
self.binary = data.get("binary", binary)
|
|
44
|
+
self.auto_location = data.get("auto_location", auto_location)
|
|
45
|
+
self.auto_timezone = data.get("auto_timezone", auto_timezone)
|
|
46
|
+
self.auto_currency = data.get("auto_currency", auto_currency)
|
|
47
|
+
self.auto_language = data.get("auto_language", auto_language)
|
|
48
|
+
self.virtual_gpu = data.get("virtual_gpu", virtual_gpu)
|
|
49
|
+
self.canvas_noise = data.get("canvas_noise", canvas_noise)
|
|
50
|
+
self.tls_spoof = data.get("tls_spoof", tls_spoof)
|
|
51
|
+
self.http3 = data.get("http3", http3)
|
|
52
|
+
self.socket_timeout = data.get("socket_timeout", socket_timeout)
|
|
53
|
+
self.storage_file = data.get("storage_file", storage_file)
|
|
54
|
+
self.viewport_width = data.get("viewport_width", viewport_width)
|
|
55
|
+
self.viewport_height = data.get("viewport_height", viewport_height)
|
|
56
|
+
self.device_scale_factor = data.get("device_scale_factor", device_scale_factor)
|
|
57
|
+
self.screen_color_depth = data.get("screen_color_depth", screen_color_depth)
|
|
58
|
+
self.hardware_concurrency = data.get("hardware_concurrency", hardware_concurrency)
|
|
59
|
+
self.device_memory = data.get("device_memory", device_memory)
|
|
60
|
+
self.max_touch_points = data.get("max_touch_points", max_touch_points)
|
|
61
|
+
self.do_not_track = data.get("do_not_track", do_not_track)
|
|
62
|
+
self.audio_sample_rate = data.get("audio_sample_rate", audio_sample_rate)
|
|
63
|
+
self.screenshot_width = data.get("screenshot_width", screenshot_width)
|
|
64
|
+
self.screenshot_height = data.get("screenshot_height", screenshot_height)
|
|
65
|
+
self.max_idle_conns = data.get("max_idle_conns", max_idle_conns)
|
|
66
|
+
self.max_idle_conns_per_host = data.get("max_idle_conns_per_host", max_idle_conns_per_host)
|
|
67
|
+
self.geo_url = data.get("geo_url", geo_url)
|
|
68
|
+
self.proxies = data.get("proxies", proxies)
|
|
69
|
+
self.max_redirects = data.get("max_redirects", max_redirects)
|
|
70
|
+
self.ca_cert = data.get("ca_cert", ca_cert)
|
|
71
|
+
self.client_cert = data.get("client_cert", client_cert)
|
|
72
|
+
self.client_key = data.get("client_key", client_key)
|
|
73
|
+
self._proxy_index = 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def build_body(data, json, headers):
|
|
77
|
+
if json is not None:
|
|
78
|
+
headers.setdefault("Content-Type", "application/json")
|
|
79
|
+
return jsonlib.dumps(json).encode()
|
|
80
|
+
if data is None:
|
|
81
|
+
return None
|
|
82
|
+
if isinstance(data, (bytes, bytearray)):
|
|
83
|
+
return bytes(data)
|
|
84
|
+
if isinstance(data, str):
|
|
85
|
+
return data.encode()
|
|
86
|
+
if isinstance(data, dict):
|
|
87
|
+
headers.setdefault("Content-Type", "application/x-www-form-urlencoded")
|
|
88
|
+
return urlencode(data, doseq=True).encode()
|
|
89
|
+
raise TypeError(f"unsupported type for data=: {type(data).__name__}, use bytes, str, dict, or json=")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def build_multipart(data, files):
|
|
93
|
+
boundary = uuid.uuid4().hex
|
|
94
|
+
parts = []
|
|
95
|
+
for k, v in (data or {}).items():
|
|
96
|
+
parts.append(f'--{boundary}\r\nContent-Disposition: form-data; name="{k}"\r\n\r\n{v}\r\n'.encode())
|
|
97
|
+
for name, item in (files or {}).items():
|
|
98
|
+
if isinstance(item, tuple):
|
|
99
|
+
filename, content = item
|
|
100
|
+
else:
|
|
101
|
+
filename, content = name, item
|
|
102
|
+
if isinstance(content, str):
|
|
103
|
+
content = content.encode()
|
|
104
|
+
head = (f'--{boundary}\r\nContent-Disposition: form-data; name="{name}"; filename="{filename}"\r\n'
|
|
105
|
+
f"Content-Type: application/octet-stream\r\n\r\n").encode()
|
|
106
|
+
parts.append(head + bytes(content) + b"\r\n")
|
|
107
|
+
parts.append(f"--{boundary}--\r\n".encode())
|
|
108
|
+
return b"".join(parts), boundary
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class Client:
|
|
112
|
+
"""Undetected, requests + Selenium-style HTTP client.
|
|
113
|
+
|
|
114
|
+
Zero-config requests-style use:
|
|
115
|
+
client = Client()
|
|
116
|
+
resp = client.get("https://example.com")
|
|
117
|
+
print(resp.status_code, resp.text, resp.json())
|
|
118
|
+
|
|
119
|
+
Selenium-style page interaction (client.get returns a page):
|
|
120
|
+
page = client.get("https://example.com/login")
|
|
121
|
+
page.fill("#username", "alice")
|
|
122
|
+
page.fill("#password", "secret")
|
|
123
|
+
page.click("button[type=submit]")
|
|
124
|
+
page.wait_for("#dashboard")
|
|
125
|
+
print(page.text)
|
|
126
|
+
page.screenshot("dashboard.png")
|
|
127
|
+
for req in page.network.requests:
|
|
128
|
+
print(req.method, req.url)
|
|
129
|
+
|
|
130
|
+
Config can be passed as keywords directly:
|
|
131
|
+
client = Client(profile="chrome-windows")
|
|
132
|
+
client = Client(proxy="socks5://user:pass@host:1080", execute_scripts=True)
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
def __init__(self, config=None, **kwargs):
|
|
136
|
+
if config is not None and kwargs:
|
|
137
|
+
raise TypeError("pass either a Config object or keyword options, not both")
|
|
138
|
+
cfg = config or Config(**kwargs) or Config()
|
|
139
|
+
self._closed = False
|
|
140
|
+
self._timeout = cfg.timeout
|
|
141
|
+
self._cfg_proxies = cfg.proxies or []
|
|
142
|
+
self._proxy_index = 0
|
|
143
|
+
|
|
144
|
+
self._conn = connection(binary=cfg.binary, verbose=cfg.verbose, solution=cfg.solution,
|
|
145
|
+
shared=cfg.shared, socket_timeout=cfg.socket_timeout)
|
|
146
|
+
self._conn.connect()
|
|
147
|
+
|
|
148
|
+
payload = {
|
|
149
|
+
"profile": cfg.profile,
|
|
150
|
+
"fingerprint": cfg.fingerprint or "",
|
|
151
|
+
"platform": cfg.platform or "",
|
|
152
|
+
"browser": cfg.browser or "",
|
|
153
|
+
"proxy": cfg.proxy or "",
|
|
154
|
+
"verify_ssl": cfg.verify_ssl,
|
|
155
|
+
"trust_env": cfg.trust_env,
|
|
156
|
+
"execute_scripts": cfg.execute_scripts,
|
|
157
|
+
"js_timeout": cfg.js_timeout,
|
|
158
|
+
"timeout": cfg.timeout,
|
|
159
|
+
"storage_file": cfg.storage_file or "",
|
|
160
|
+
"virtual_gpu": cfg.virtual_gpu,
|
|
161
|
+
"canvas_noise": cfg.canvas_noise,
|
|
162
|
+
"http3": cfg.http3,
|
|
163
|
+
}
|
|
164
|
+
if cfg.auto_location is not None:
|
|
165
|
+
payload["auto_location"] = cfg.auto_location
|
|
166
|
+
if cfg.auto_timezone is not None:
|
|
167
|
+
payload["auto_timezone"] = cfg.auto_timezone
|
|
168
|
+
if cfg.auto_currency is not None:
|
|
169
|
+
payload["auto_currency"] = cfg.auto_currency
|
|
170
|
+
if cfg.auto_language is not None:
|
|
171
|
+
payload["auto_language"] = cfg.auto_language
|
|
172
|
+
if cfg.tls_spoof is not None:
|
|
173
|
+
payload["tls_spoof"] = cfg.tls_spoof
|
|
174
|
+
|
|
175
|
+
for key, val in [
|
|
176
|
+
("viewport_width", cfg.viewport_width),
|
|
177
|
+
("viewport_height", cfg.viewport_height),
|
|
178
|
+
("device_scale_factor", cfg.device_scale_factor),
|
|
179
|
+
("screen_color_depth", cfg.screen_color_depth),
|
|
180
|
+
("hardware_concurrency", cfg.hardware_concurrency),
|
|
181
|
+
("device_memory", cfg.device_memory),
|
|
182
|
+
("max_touch_points", cfg.max_touch_points),
|
|
183
|
+
("audio_sample_rate", cfg.audio_sample_rate),
|
|
184
|
+
("screenshot_width", cfg.screenshot_width),
|
|
185
|
+
("screenshot_height", cfg.screenshot_height),
|
|
186
|
+
("max_idle_conns", cfg.max_idle_conns),
|
|
187
|
+
("max_idle_conns_per_host", cfg.max_idle_conns_per_host),
|
|
188
|
+
("max_redirects", cfg.max_redirects),
|
|
189
|
+
]:
|
|
190
|
+
if val is not None:
|
|
191
|
+
payload[key] = val
|
|
192
|
+
if cfg.geo_url is not None:
|
|
193
|
+
payload["geo_url"] = cfg.geo_url
|
|
194
|
+
for key, val in [("ca_cert", cfg.ca_cert), ("client_cert", cfg.client_cert), ("client_key", cfg.client_key)]:
|
|
195
|
+
if val:
|
|
196
|
+
payload[key] = val
|
|
197
|
+
if cfg.do_not_track is not None:
|
|
198
|
+
payload["do_not_track"] = cfg.do_not_track
|
|
199
|
+
|
|
200
|
+
result = self._conn.call("session.create", payload)
|
|
201
|
+
self._session = Session(self._conn, result["session_id"], cfg.profile, cfg.fingerprint)
|
|
202
|
+
|
|
203
|
+
def get(self, url, headers=None, timeout=None, proxy=None, fingerprint=None, platform=None, browser=None, profile=None):
|
|
204
|
+
return self._request("GET", url, headers=headers, timeout=timeout, proxy=proxy,
|
|
205
|
+
fingerprint=fingerprint, platform=platform, browser=browser, profile=profile)
|
|
206
|
+
|
|
207
|
+
def post(self, url, data=None, json=None, files=None, headers=None, timeout=None, proxy=None,
|
|
208
|
+
fingerprint=None, platform=None, browser=None, profile=None):
|
|
209
|
+
headers = dict(headers or {})
|
|
210
|
+
if files is not None:
|
|
211
|
+
mpbody, boundary = build_multipart(data, files)
|
|
212
|
+
headers["Content-Type"] = f"multipart/form-data; boundary={boundary}"
|
|
213
|
+
else:
|
|
214
|
+
mpbody = build_body(data, json, headers)
|
|
215
|
+
return self._request("POST", url, headers=headers, body=mpbody, timeout=timeout, proxy=proxy,
|
|
216
|
+
fingerprint=fingerprint, platform=platform, browser=browser, profile=profile)
|
|
217
|
+
|
|
218
|
+
def put(self, url, data=None, json=None, files=None, headers=None, timeout=None, proxy=None,
|
|
219
|
+
fingerprint=None, platform=None, browser=None, profile=None):
|
|
220
|
+
headers = dict(headers or {})
|
|
221
|
+
if files is not None:
|
|
222
|
+
mpbody, boundary = build_multipart(data, files)
|
|
223
|
+
headers["Content-Type"] = f"multipart/form-data; boundary={boundary}"
|
|
224
|
+
else:
|
|
225
|
+
mpbody = build_body(data, json, headers)
|
|
226
|
+
return self._request("PUT", url, headers=headers, body=mpbody, timeout=timeout, proxy=proxy,
|
|
227
|
+
fingerprint=fingerprint, platform=platform, browser=browser, profile=profile)
|
|
228
|
+
|
|
229
|
+
def patch(self, url, data=None, json=None, files=None, headers=None, timeout=None, proxy=None,
|
|
230
|
+
fingerprint=None, platform=None, browser=None, profile=None):
|
|
231
|
+
headers = dict(headers or {})
|
|
232
|
+
if files is not None:
|
|
233
|
+
mpbody, boundary = build_multipart(data, files)
|
|
234
|
+
headers["Content-Type"] = f"multipart/form-data; boundary={boundary}"
|
|
235
|
+
else:
|
|
236
|
+
mpbody = build_body(data, json, headers)
|
|
237
|
+
return self._request("PATCH", url, headers=headers, body=mpbody, timeout=timeout, proxy=proxy,
|
|
238
|
+
fingerprint=fingerprint, platform=platform, browser=browser, profile=profile)
|
|
239
|
+
|
|
240
|
+
def delete(self, url, headers=None, timeout=None, proxy=None, fingerprint=None, platform=None, browser=None, profile=None):
|
|
241
|
+
return self._request("DELETE", url, headers=headers, timeout=timeout, proxy=proxy,
|
|
242
|
+
fingerprint=fingerprint, platform=platform, browser=browser, profile=profile)
|
|
243
|
+
|
|
244
|
+
def head(self, url, headers=None, timeout=None, proxy=None, fingerprint=None, platform=None, browser=None, profile=None):
|
|
245
|
+
return self._request("HEAD", url, headers=headers, timeout=timeout, proxy=proxy,
|
|
246
|
+
fingerprint=fingerprint, platform=platform, browser=browser, profile=profile)
|
|
247
|
+
|
|
248
|
+
def options(self, url, headers=None, timeout=None, proxy=None, fingerprint=None, platform=None, browser=None, profile=None):
|
|
249
|
+
return self._request("OPTIONS", url, headers=headers, timeout=timeout, proxy=proxy,
|
|
250
|
+
fingerprint=fingerprint, platform=platform, browser=browser, profile=profile)
|
|
251
|
+
|
|
252
|
+
def _request(self, method, url, headers=None, body=None, timeout=None, proxy=None,
|
|
253
|
+
fingerprint=None, platform=None, browser=None, profile=None):
|
|
254
|
+
if self._closed:
|
|
255
|
+
raise LithiumError("this client is closed, create a new Client() to make more requests")
|
|
256
|
+
|
|
257
|
+
if proxy is None and self._cfg_proxies:
|
|
258
|
+
proxy = self._next_proxy()
|
|
259
|
+
|
|
260
|
+
params = {
|
|
261
|
+
"session_id": self._session.id,
|
|
262
|
+
"method": method,
|
|
263
|
+
"url": url,
|
|
264
|
+
"headers": headers or {},
|
|
265
|
+
"timeout": timeout if timeout is not None else self._timeout,
|
|
266
|
+
"proxy": proxy or "",
|
|
267
|
+
"fingerprint": fingerprint or "",
|
|
268
|
+
"platform": platform or "",
|
|
269
|
+
"browser": browser or "",
|
|
270
|
+
"profile": profile or "",
|
|
271
|
+
}
|
|
272
|
+
if body is not None:
|
|
273
|
+
params["body"] = body
|
|
274
|
+
|
|
275
|
+
data = self._conn.call("http.fetch", params)
|
|
276
|
+
return Response(data, conn=self._conn, session_id=self._session.id)
|
|
277
|
+
|
|
278
|
+
def _next_proxy(self):
|
|
279
|
+
pool = self._cfg_proxies
|
|
280
|
+
p = pool[self._proxy_index % len(pool)]
|
|
281
|
+
self._proxy_index += 1
|
|
282
|
+
return p
|
|
283
|
+
|
|
284
|
+
def stream_to_file(self, url, path, method="GET", headers=None, body=None, timeout=None,
|
|
285
|
+
proxy=None, fingerprint=None, platform=None, browser=None, profile=None):
|
|
286
|
+
if self._closed:
|
|
287
|
+
raise LithiumError("this client is closed, create a new Client() to make more requests")
|
|
288
|
+
if proxy is None and self._cfg_proxies:
|
|
289
|
+
proxy = self._next_proxy()
|
|
290
|
+
params = {
|
|
291
|
+
"session_id": self._session.id,
|
|
292
|
+
"method": method,
|
|
293
|
+
"url": url,
|
|
294
|
+
"headers": headers or {},
|
|
295
|
+
"timeout": timeout if timeout is not None else self._timeout,
|
|
296
|
+
"proxy": proxy or "",
|
|
297
|
+
"fingerprint": fingerprint or "",
|
|
298
|
+
"platform": platform or "",
|
|
299
|
+
"browser": browser or "",
|
|
300
|
+
"profile": profile or "",
|
|
301
|
+
"path": path,
|
|
302
|
+
}
|
|
303
|
+
if body is not None:
|
|
304
|
+
params["body"] = body
|
|
305
|
+
data = self._conn.call("http.download", params)
|
|
306
|
+
return Response(data, conn=self._conn, session_id=self._session.id)
|
|
307
|
+
|
|
308
|
+
def download(self, url, path, timeout=None, proxy=None):
|
|
309
|
+
response = self.get(url, timeout=timeout, proxy=proxy)
|
|
310
|
+
with open(path, "wb") as f:
|
|
311
|
+
f.write(response.content)
|
|
312
|
+
return path
|
|
313
|
+
|
|
314
|
+
@property
|
|
315
|
+
def network(self):
|
|
316
|
+
if self._closed:
|
|
317
|
+
raise LithiumError("this client is closed, create a new Client() to inspect network activity")
|
|
318
|
+
data = self._conn.call("network.requests", {"session_id": self._session.id})
|
|
319
|
+
return NetworkLog(data.get("requests", []))
|
|
320
|
+
|
|
321
|
+
def close(self):
|
|
322
|
+
if self._closed:
|
|
323
|
+
return
|
|
324
|
+
self._closed = True
|
|
325
|
+
try:
|
|
326
|
+
self._session.close()
|
|
327
|
+
finally:
|
|
328
|
+
self._conn.close()
|
|
329
|
+
|
|
330
|
+
def __enter__(self):
|
|
331
|
+
return self
|
|
332
|
+
|
|
333
|
+
def __exit__(self, *exc):
|
|
334
|
+
self.close()
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class AsyncClient:
|
|
338
|
+
|
|
339
|
+
def __init__(self, config=None, **kwargs):
|
|
340
|
+
self._client = Client(config=config, **kwargs)
|
|
341
|
+
|
|
342
|
+
async def get(self, *a, **k):
|
|
343
|
+
return await asyncio.to_thread(self._client.get, *a, **k)
|
|
344
|
+
|
|
345
|
+
async def post(self, *a, **k):
|
|
346
|
+
return await asyncio.to_thread(self._client.post, *a, **k)
|
|
347
|
+
|
|
348
|
+
async def put(self, *a, **k):
|
|
349
|
+
return await asyncio.to_thread(self._client.put, *a, **k)
|
|
350
|
+
|
|
351
|
+
async def patch(self, *a, **k):
|
|
352
|
+
return await asyncio.to_thread(self._client.patch, *a, **k)
|
|
353
|
+
|
|
354
|
+
async def delete(self, *a, **k):
|
|
355
|
+
return await asyncio.to_thread(self._client.delete, *a, **k)
|
|
356
|
+
|
|
357
|
+
async def head(self, *a, **k):
|
|
358
|
+
return await asyncio.to_thread(self._client.head, *a, **k)
|
|
359
|
+
|
|
360
|
+
async def options(self, *a, **k):
|
|
361
|
+
return await asyncio.to_thread(self._client.options, *a, **k)
|
|
362
|
+
|
|
363
|
+
async def stream_to_file(self, *a, **k):
|
|
364
|
+
return await asyncio.to_thread(self._client.stream_to_file, *a, **k)
|
|
365
|
+
|
|
366
|
+
async def download(self, *a, **k):
|
|
367
|
+
return await asyncio.to_thread(self._client.download, *a, **k)
|
|
368
|
+
|
|
369
|
+
async def close(self):
|
|
370
|
+
self._client.close()
|
|
371
|
+
|
|
372
|
+
async def __aenter__(self):
|
|
373
|
+
return self
|
|
374
|
+
|
|
375
|
+
async def __aexit__(self, *exc):
|
|
376
|
+
await self.close()
|
|
377
|
+
|
|
378
|
+
@property
|
|
379
|
+
def network(self):
|
|
380
|
+
return self._client.network
|