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