seleniumbase 4.32.0__py3-none-any.whl → 4.32.2__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,830 @@
1
+ """CDP-Driver is based on NoDriver"""
2
+ from __future__ import annotations
3
+ import asyncio
4
+ import atexit
5
+ import http.cookiejar
6
+ import json
7
+ import logging
8
+ import os
9
+ import pickle
10
+ import pathlib
11
+ import shutil
12
+ import urllib.parse
13
+ import urllib.request
14
+ import warnings
15
+ from collections import defaultdict
16
+ from typing import List, Set, Tuple, Union
17
+ import mycdp as cdp
18
+ from . import cdp_util as util
19
+ from . import tab
20
+ from ._contradict import ContraDict
21
+ from .config import PathLike, Config, is_posix
22
+ from .connection import Connection
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ def get_registered_instances():
28
+ return __registered__instances__
29
+
30
+
31
+ def deconstruct_browser():
32
+ import time
33
+
34
+ for _ in __registered__instances__:
35
+ if not _.stopped:
36
+ _.stop()
37
+ for attempt in range(5):
38
+ try:
39
+ if _.config and not _.config.uses_custom_data_dir:
40
+ shutil.rmtree(_.config.user_data_dir, ignore_errors=False)
41
+ except FileNotFoundError:
42
+ break
43
+ except (PermissionError, OSError) as e:
44
+ if attempt == 4:
45
+ logger.debug(
46
+ "Problem removing data dir %s\n"
47
+ "Consider checking whether it's there "
48
+ "and remove it by hand\nerror: %s",
49
+ _.config.user_data_dir,
50
+ e,
51
+ )
52
+ break
53
+ time.sleep(0.15)
54
+ continue
55
+ logging.debug("Temp profile %s was removed." % _.config.user_data_dir)
56
+
57
+
58
+ class Browser:
59
+ """
60
+ The Browser object is the "root" of the hierarchy
61
+ and contains a reference to the browser parent process.
62
+ There should usually be only 1 instance of this.
63
+ All opened tabs, extra browser screens,
64
+ and resources will not cause a new Browser process,
65
+ but rather create additional :class:`Tab` objects.
66
+ So, besides starting your instance and first/additional tabs,
67
+ you don't actively use it a lot under normal conditions.
68
+ Tab objects will represent and control:
69
+ - tabs (as you know them)
70
+ - browser windows (new window)
71
+ - iframe
72
+ - background processes
73
+ Note:
74
+ The Browser object is not instantiated by __init__
75
+ but using the asynchronous :meth:`Browser.create` method.
76
+ Note:
77
+ In Chromium based browsers, there is a parent process which keeps
78
+ running all the time, even if there are no visible browser windows.
79
+ Sometimes it's stubborn to close it, so make sure that after using
80
+ this library, the browser is correctly and fully closed/exited/killed.
81
+ """
82
+ _process: asyncio.subprocess.Process
83
+ _process_pid: int
84
+ _http: HTTPApi = None
85
+ _cookies: CookieJar = None
86
+ config: Config
87
+ connection: Connection
88
+
89
+ @classmethod
90
+ async def create(
91
+ cls,
92
+ config: Config = None,
93
+ *,
94
+ user_data_dir: PathLike = None,
95
+ headless: bool = False,
96
+ incognito: bool = False,
97
+ guest: bool = False,
98
+ browser_executable_path: PathLike = None,
99
+ browser_args: List[str] = None,
100
+ sandbox: bool = True,
101
+ host: str = None,
102
+ port: int = None,
103
+ **kwargs,
104
+ ) -> Browser:
105
+ """Entry point for creating an instance."""
106
+ if not config:
107
+ config = Config(
108
+ user_data_dir=user_data_dir,
109
+ headless=headless,
110
+ incognito=incognito,
111
+ guest=guest,
112
+ browser_executable_path=browser_executable_path,
113
+ browser_args=browser_args or [],
114
+ sandbox=sandbox,
115
+ host=host,
116
+ port=port,
117
+ **kwargs,
118
+ )
119
+ instance = cls(config)
120
+ await instance.start()
121
+ return instance
122
+
123
+ def __init__(self, config: Config, **kwargs):
124
+ """
125
+ Constructor. To create a instance, use :py:meth:`Browser.create(...)`
126
+ :param config:
127
+ """
128
+ try:
129
+ asyncio.get_running_loop()
130
+ except RuntimeError:
131
+ raise RuntimeError(
132
+ "{0} objects of this class are created "
133
+ "using await {0}.create()".format(
134
+ self.__class__.__name__
135
+ )
136
+ )
137
+ self.config = config
138
+ self.targets: List = []
139
+ self.info = None
140
+ self._target = None
141
+ self._process = None
142
+ self._process_pid = None
143
+ self._keep_user_data_dir = None
144
+ self._is_updating = asyncio.Event()
145
+ self.connection: Connection = None
146
+ logger.debug("Session object initialized: %s" % vars(self))
147
+
148
+ @property
149
+ def websocket_url(self):
150
+ return self.info.webSocketDebuggerUrl
151
+
152
+ @property
153
+ def main_tab(self) -> tab.Tab:
154
+ """Returns the target which was launched with the browser."""
155
+ return sorted(
156
+ self.targets, key=lambda x: x.type_ == "page", reverse=True
157
+ )[0]
158
+
159
+ @property
160
+ def tabs(self) -> List[tab.Tab]:
161
+ """Returns the current targets which are of type "page"."""
162
+ tabs = filter(lambda item: item.type_ == "page", self.targets)
163
+ return list(tabs)
164
+
165
+ @property
166
+ def cookies(self) -> CookieJar:
167
+ if not self._cookies:
168
+ self._cookies = CookieJar(self)
169
+ return self._cookies
170
+
171
+ @property
172
+ def stopped(self):
173
+ if self._process and self._process.returncode is None:
174
+ return False
175
+ return True
176
+ # return (self._process and self._process.returncode) or False
177
+
178
+ async def wait(self, time: Union[float, int] = 1) -> Browser:
179
+ """Wait for <time> seconds. Important to use,
180
+ especially in between page navigation.
181
+ :param time:
182
+ """
183
+ return await asyncio.sleep(time, result=self)
184
+
185
+ sleep = wait
186
+ """Alias for wait"""
187
+ def _handle_target_update(
188
+ self,
189
+ event: Union[
190
+ cdp.target.TargetInfoChanged,
191
+ cdp.target.TargetDestroyed,
192
+ cdp.target.TargetCreated,
193
+ cdp.target.TargetCrashed,
194
+ ],
195
+ ):
196
+ """This is an internal handler which updates the targets
197
+ when Chrome emits the corresponding event."""
198
+ if isinstance(event, cdp.target.TargetInfoChanged):
199
+ target_info = event.target_info
200
+ current_tab = next(
201
+ filter(
202
+ lambda item: item.target_id == target_info.target_id, self.targets # noqa
203
+ )
204
+ )
205
+ current_target = current_tab.target
206
+ if logger.getEffectiveLevel() <= 10:
207
+ changes = util.compare_target_info(
208
+ current_target, target_info
209
+ )
210
+ changes_string = ""
211
+ for change in changes:
212
+ key, old, new = change
213
+ changes_string += f"\n{key}: {old} => {new}\n"
214
+ logger.debug(
215
+ "Target #%d has changed: %s"
216
+ % (self.targets.index(current_tab), changes_string)
217
+ )
218
+ current_tab.target = target_info
219
+ elif isinstance(event, cdp.target.TargetCreated):
220
+ target_info: cdp.target.TargetInfo = event.target_info
221
+ from .tab import Tab
222
+
223
+ new_target = Tab(
224
+ (
225
+ f"ws://{self.config.host}:{self.config.port}"
226
+ f"/devtools/{target_info.type_ or 'page'}"
227
+ f"/{target_info.target_id}"
228
+ ),
229
+ target=target_info,
230
+ browser=self,
231
+ )
232
+ self.targets.append(new_target)
233
+ logger.debug(
234
+ "Target #%d created => %s", len(self.targets), new_target
235
+ )
236
+ elif isinstance(event, cdp.target.TargetDestroyed):
237
+ current_tab = next(
238
+ filter(
239
+ lambda item: item.target_id == event.target_id,
240
+ self.targets,
241
+ )
242
+ )
243
+ logger.debug(
244
+ "Target removed. id # %d => %s"
245
+ % (self.targets.index(current_tab), current_tab)
246
+ )
247
+ self.targets.remove(current_tab)
248
+
249
+ async def get(
250
+ self,
251
+ url="chrome://welcome",
252
+ new_tab: bool = False,
253
+ new_window: bool = False,
254
+ ) -> tab.Tab:
255
+ """Top level get. Utilizes the first tab to retrieve given url.
256
+ Convenience function known from selenium.
257
+ This function detects when DOM events have fired during navigation.
258
+ :param url: The URL to navigate to
259
+ :param new_tab: Open new tab
260
+ :param new_window: Open new window
261
+ :return: Page
262
+ """
263
+ if new_tab or new_window:
264
+ # Create new target using the browser session.
265
+ target_id = await self.connection.send(
266
+ cdp.target.create_target(
267
+ url, new_window=new_window, enable_begin_frame_control=True
268
+ )
269
+ )
270
+ connection: tab.Tab = next(
271
+ filter(
272
+ lambda item: item.type_ == "page" and item.target_id == target_id, # noqa
273
+ self.targets,
274
+ )
275
+ )
276
+ connection.browser = self
277
+ else:
278
+ # First tab from browser.tabs
279
+ connection: tab.Tab = next(
280
+ filter(lambda item: item.type_ == "page", self.targets)
281
+ )
282
+ # Use the tab to navigate to new url
283
+ frame_id, loader_id, *_ = await connection.send(
284
+ cdp.page.navigate(url)
285
+ )
286
+ # Update the frame_id on the tab
287
+ connection.frame_id = frame_id
288
+ connection.browser = self
289
+ await connection.sleep(0.25)
290
+ return connection
291
+
292
+ async def start(self=None) -> Browser:
293
+ """Launches the actual browser."""
294
+ if not self:
295
+ warnings.warn(
296
+ "Use ``await Browser.create()`` to create a new instance!"
297
+ )
298
+ return
299
+ if self._process or self._process_pid:
300
+ if self._process.returncode is not None:
301
+ return await self.create(config=self.config)
302
+ warnings.warn(
303
+ "Ignored! This call has no effect when already running!"
304
+ )
305
+ return
306
+ # self.config.update(kwargs)
307
+ connect_existing = False
308
+ if self.config.host is not None and self.config.port is not None:
309
+ connect_existing = True
310
+ else:
311
+ self.config.host = "127.0.0.1"
312
+ self.config.port = util.free_port()
313
+ if not connect_existing:
314
+ logger.debug(
315
+ "BROWSER EXECUTABLE PATH: %s",
316
+ self.config.browser_executable_path,
317
+ )
318
+ if not pathlib.Path(self.config.browser_executable_path).exists():
319
+ raise FileNotFoundError(
320
+ (
321
+ """
322
+ ---------------------------------------
323
+ Could not determine browser executable.
324
+ ---------------------------------------
325
+ Browser must be installed in the default location / path!
326
+ If you are sure about the browser executable,
327
+ set it using `browser_executable_path='{}` parameter."""
328
+ ).format(
329
+ "/path/to/browser/executable"
330
+ if is_posix
331
+ else "c:/path/to/your/browser.exe"
332
+ )
333
+ )
334
+ if getattr(self.config, "_extensions", None): # noqa
335
+ self.config.add_argument(
336
+ "--load-extension=%s"
337
+ % ",".join(str(_) for _ in self.config._extensions)
338
+ ) # noqa
339
+ exe = self.config.browser_executable_path
340
+ params = self.config()
341
+ logger.info(
342
+ "Starting\n\texecutable :%s\n\narguments:\n%s",
343
+ exe,
344
+ "\n\t".join(params),
345
+ )
346
+ if not connect_existing:
347
+ self._process: asyncio.subprocess.Process = (
348
+ await asyncio.create_subprocess_exec(
349
+ # self.config.browser_executable_path,
350
+ # *cmdparams,
351
+ exe,
352
+ *params,
353
+ stdin=asyncio.subprocess.PIPE,
354
+ stdout=asyncio.subprocess.PIPE,
355
+ stderr=asyncio.subprocess.PIPE,
356
+ close_fds=is_posix,
357
+ )
358
+ )
359
+ self._process_pid = self._process.pid
360
+ self._http = HTTPApi((self.config.host, self.config.port))
361
+ get_registered_instances().add(self)
362
+ await asyncio.sleep(0.25)
363
+ for _ in range(5):
364
+ try:
365
+ self.info = ContraDict(
366
+ await self._http.get("version"), silent=True
367
+ )
368
+ except (Exception,):
369
+ if _ == 4:
370
+ logger.debug("Could not start", exc_info=True)
371
+ await self.sleep(0.5)
372
+ else:
373
+ break
374
+ if not self.info:
375
+ raise Exception(
376
+ (
377
+ """
378
+ --------------------------------
379
+ Failed to connect to the browser
380
+ --------------------------------
381
+ Possibly because you are running as "root".
382
+ If so, you may need to use no_sandbox=True.
383
+ """
384
+ )
385
+ )
386
+ self.connection = Connection(
387
+ self.info.webSocketDebuggerUrl, _owner=self
388
+ )
389
+ if self.config.autodiscover_targets:
390
+ logger.info("Enabling autodiscover targets")
391
+ self.connection.handlers[cdp.target.TargetInfoChanged] = [
392
+ self._handle_target_update
393
+ ]
394
+ self.connection.handlers[cdp.target.TargetCreated] = [
395
+ self._handle_target_update
396
+ ]
397
+ self.connection.handlers[cdp.target.TargetDestroyed] = [
398
+ self._handle_target_update
399
+ ]
400
+ self.connection.handlers[cdp.target.TargetCrashed] = [
401
+ self._handle_target_update
402
+ ]
403
+ await self.connection.send(
404
+ cdp.target.set_discover_targets(discover=True)
405
+ )
406
+ await self
407
+ # self.connection.handlers[cdp.inspector.Detached] = [self.stop]
408
+ # return self
409
+
410
+ async def grant_all_permissions(self):
411
+ """
412
+ Grant permissions for:
413
+ accessibilityEvents
414
+ audioCapture
415
+ backgroundSync
416
+ backgroundFetch
417
+ clipboardReadWrite
418
+ clipboardSanitizedWrite
419
+ displayCapture
420
+ durableStorage
421
+ geolocation
422
+ idleDetection
423
+ localFonts
424
+ midi
425
+ midiSysex
426
+ nfc
427
+ notifications
428
+ paymentHandler
429
+ periodicBackgroundSync
430
+ protectedMediaIdentifier
431
+ sensors
432
+ storageAccess
433
+ topLevelStorageAccess
434
+ videoCapture
435
+ videoCapturePanTiltZoom
436
+ wakeLockScreen
437
+ wakeLockSystem
438
+ windowManagement
439
+ """
440
+ permissions = list(cdp.browser.PermissionType)
441
+ permissions.remove(cdp.browser.PermissionType.FLASH)
442
+ permissions.remove(cdp.browser.PermissionType.CAPTURED_SURFACE_CONTROL)
443
+ await self.connection.send(cdp.browser.grant_permissions(permissions))
444
+
445
+ async def tile_windows(self, windows=None, max_columns: int = 0):
446
+ import math
447
+ try:
448
+ import mss
449
+ except Exception:
450
+ from seleniumbase.fixtures import shared_utils
451
+ shared_utils.pip_install("mss")
452
+ import mss
453
+ m = mss.mss()
454
+ screen, screen_width, screen_height = 3 * (None,)
455
+ if m.monitors and len(m.monitors) >= 1:
456
+ screen = m.monitors[0]
457
+ screen_width = screen["width"]
458
+ screen_height = screen["height"]
459
+ if not screen or not screen_width or not screen_height:
460
+ warnings.warn("No monitors detected!")
461
+ return
462
+ await self
463
+ distinct_windows = defaultdict(list)
464
+ if windows:
465
+ tabs = windows
466
+ else:
467
+ tabs = self.tabs
468
+ for _tab in tabs:
469
+ window_id, bounds = await _tab.get_window()
470
+ distinct_windows[window_id].append(_tab)
471
+ num_windows = len(distinct_windows)
472
+ req_cols = max_columns or int(num_windows * (19 / 6))
473
+ req_rows = int(num_windows / req_cols)
474
+ while req_cols * req_rows < num_windows:
475
+ req_rows += 1
476
+ box_w = math.floor((screen_width / req_cols) - 1)
477
+ box_h = math.floor(screen_height / req_rows)
478
+ distinct_windows_iter = iter(distinct_windows.values())
479
+ grid = []
480
+ for x in range(req_cols):
481
+ for y in range(req_rows):
482
+ try:
483
+ tabs = next(distinct_windows_iter)
484
+ except StopIteration:
485
+ continue
486
+ if not tabs:
487
+ continue
488
+ tab = tabs[0]
489
+ try:
490
+ pos = [x * box_w, y * box_h, box_w, box_h]
491
+ grid.append(pos)
492
+ await tab.set_window_size(*pos)
493
+ except Exception:
494
+ logger.info(
495
+ "Could not set window size. Exception => ",
496
+ exc_info=True,
497
+ )
498
+ continue
499
+ return grid
500
+
501
+ async def _get_targets(self) -> List[cdp.target.TargetInfo]:
502
+ info = await self.connection.send(
503
+ cdp.target.get_targets(), _is_update=True
504
+ )
505
+ return info
506
+
507
+ async def update_targets(self):
508
+ targets: List[cdp.target.TargetInfo]
509
+ targets = await self._get_targets()
510
+ for t in targets:
511
+ for existing_tab in self.targets:
512
+ existing_target = existing_tab.target
513
+ if existing_target.target_id == t.target_id:
514
+ existing_tab.target.__dict__.update(t.__dict__)
515
+ break
516
+ else:
517
+ self.targets.append(
518
+ Connection(
519
+ (
520
+ f"ws://{self.config.host}:{self.config.port}"
521
+ f"/devtools/page" # All types are "page"
522
+ f"/{t.target_id}"
523
+ ),
524
+ target=t,
525
+ _owner=self,
526
+ )
527
+ )
528
+ await asyncio.sleep(0)
529
+
530
+ async def __aenter__(self):
531
+ return self
532
+
533
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
534
+ if exc_type and exc_val:
535
+ raise exc_type(exc_val)
536
+
537
+ def __iter__(self):
538
+ self._i = self.tabs.index(self.main_tab)
539
+ return self
540
+
541
+ def __reversed__(self):
542
+ return reversed(list(self.tabs))
543
+
544
+ def __next__(self):
545
+ try:
546
+ return self.tabs[self._i]
547
+ except IndexError:
548
+ del self._i
549
+ raise StopIteration
550
+ except AttributeError:
551
+ del self._i
552
+ raise StopIteration
553
+ finally:
554
+ if hasattr(self, "_i"):
555
+ if self._i != len(self.tabs):
556
+ self._i += 1
557
+ else:
558
+ del self._i
559
+
560
+ def stop(self):
561
+ try:
562
+ # asyncio.get_running_loop().create_task(
563
+ # self.connection.send(cdp.browser.close())
564
+ # )
565
+ asyncio.get_event_loop().create_task(self.connection.aclose())
566
+ logger.debug(
567
+ "Closed the connection using get_event_loop().create_task()"
568
+ )
569
+ except RuntimeError:
570
+ if self.connection:
571
+ try:
572
+ # asyncio.run(self.connection.send(cdp.browser.close()))
573
+ asyncio.run(self.connection.aclose())
574
+ logger.debug("Closed the connection using asyncio.run()")
575
+ except Exception:
576
+ pass
577
+ for _ in range(3):
578
+ try:
579
+ self._process.terminate()
580
+ logger.info(
581
+ "Terminated browser with pid %d successfully."
582
+ % self._process.pid
583
+ )
584
+ break
585
+ except (Exception,):
586
+ try:
587
+ self._process.kill()
588
+ logger.info(
589
+ "Killed browser with pid %d successfully."
590
+ % self._process.pid
591
+ )
592
+ break
593
+ except (Exception,):
594
+ try:
595
+ if hasattr(self, "browser_process_pid"):
596
+ os.kill(self._process_pid, 15)
597
+ logger.info(
598
+ "Killed browser with pid %d "
599
+ "using signal 15 successfully."
600
+ % self._process.pid
601
+ )
602
+ break
603
+ except (TypeError,):
604
+ logger.info("typerror", exc_info=True)
605
+ pass
606
+ except (PermissionError,):
607
+ logger.info(
608
+ "Browser already stopped, "
609
+ "or no permission to kill. Skip."
610
+ )
611
+ pass
612
+ except (ProcessLookupError,):
613
+ logger.info("Process lookup failure!")
614
+ pass
615
+ except (Exception,):
616
+ raise
617
+ self._process = None
618
+ self._process_pid = None
619
+
620
+ def __await__(self):
621
+ # return ( asyncio.sleep(0)).__await__()
622
+ return self.update_targets().__await__()
623
+
624
+ def __del__(self):
625
+ pass
626
+
627
+
628
+ __registered__instances__: Set[Browser] = set()
629
+
630
+
631
+ class CookieJar:
632
+ def __init__(self, browser: Browser):
633
+ self._browser = browser
634
+
635
+ async def get_all(
636
+ self, requests_cookie_format: bool = False
637
+ ) -> List[Union[cdp.network.Cookie, "http.cookiejar.Cookie"]]:
638
+ """
639
+ Get all cookies.
640
+ :param requests_cookie_format: when True,
641
+ returns python http.cookiejar.Cookie objects,
642
+ compatible with requests library and many others.
643
+ :type requests_cookie_format: bool
644
+ """
645
+ connection = None
646
+ for _tab in self._browser.tabs:
647
+ if _tab.closed:
648
+ continue
649
+ connection = _tab
650
+ break
651
+ else:
652
+ connection = self._browser.connection
653
+ cookies = await connection.send(cdp.storage.get_cookies())
654
+ if requests_cookie_format:
655
+ import requests.cookies
656
+
657
+ return [
658
+ requests.cookies.create_cookie(
659
+ name=c.name,
660
+ value=c.value,
661
+ domain=c.domain,
662
+ path=c.path,
663
+ expires=c.expires,
664
+ secure=c.secure,
665
+ )
666
+ for c in cookies
667
+ ]
668
+ return cookies
669
+
670
+ async def set_all(self, cookies: List[cdp.network.CookieParam]):
671
+ """
672
+ Set cookies.
673
+ :param cookies: List of cookies
674
+ """
675
+ connection = None
676
+ for _tab in self._browser.tabs:
677
+ if _tab.closed:
678
+ continue
679
+ connection = _tab
680
+ break
681
+ else:
682
+ connection = self._browser.connection
683
+ cookies = await connection.send(cdp.storage.get_cookies())
684
+ await connection.send(cdp.storage.set_cookies(cookies))
685
+
686
+ async def save(self, file: PathLike = ".session.dat", pattern: str = ".*"):
687
+ """
688
+ Save all cookies (or a subset, controlled by `pattern`)
689
+ to a file to be restored later.
690
+ :param file:
691
+ :param pattern: regex style pattern string.
692
+ any cookie that has a domain, key or value field
693
+ which matches the pattern will be included.
694
+ default = ".*" (all)
695
+ Eg: the pattern "(cf|.com|nowsecure)" will include cookies which:
696
+ - Have a string "cf" (cloudflare)
697
+ - Have ".com" in them, in either domain, key or value field.
698
+ - Contain "nowsecure"
699
+ :type pattern: str
700
+ """
701
+ import re
702
+
703
+ pattern = re.compile(pattern)
704
+ save_path = pathlib.Path(file).resolve()
705
+ connection = None
706
+ for _tab in self._browser.tabs:
707
+ if _tab.closed:
708
+ continue
709
+ connection = _tab
710
+ break
711
+ else:
712
+ connection = self._browser.connection
713
+ cookies = await connection.send(cdp.storage.get_cookies())
714
+ # if not connection:
715
+ # return
716
+ # if not connection.websocket:
717
+ # return
718
+ # if connection.websocket.closed:
719
+ # return
720
+ cookies = await self.get_all(requests_cookie_format=False)
721
+ included_cookies = []
722
+ for cookie in cookies:
723
+ for match in pattern.finditer(str(cookie.__dict__)):
724
+ logger.debug(
725
+ "Saved cookie for matching pattern '%s' => (%s: %s)",
726
+ pattern.pattern,
727
+ cookie.name,
728
+ cookie.value,
729
+ )
730
+ included_cookies.append(cookie)
731
+ break
732
+ pickle.dump(cookies, save_path.open("w+b"))
733
+
734
+ async def load(self, file: PathLike = ".session.dat", pattern: str = ".*"):
735
+ """
736
+ Load all cookies (or a subset, controlled by `pattern`)
737
+ from a file created by :py:meth:`~save_cookies`.
738
+ :param file:
739
+ :param pattern: Regex style pattern string.
740
+ Any cookie that has a domain, key,
741
+ or value field which matches the pattern will be included.
742
+ Default = ".*" (all)
743
+ Eg: the pattern "(cf|.com|nowsecure)" will include cookies which:
744
+ - Have a string "cf" (cloudflare)
745
+ - Have ".com" in them, in either domain, key or value field.
746
+ - Contain "nowsecure"
747
+ :type pattern: str
748
+ """
749
+ import re
750
+
751
+ pattern = re.compile(pattern)
752
+ save_path = pathlib.Path(file).resolve()
753
+ cookies = pickle.load(save_path.open("r+b"))
754
+ included_cookies = []
755
+ connection = None
756
+ for _tab in self._browser.tabs:
757
+ if _tab.closed:
758
+ continue
759
+ connection = _tab
760
+ break
761
+ else:
762
+ connection = self._browser.connection
763
+ for cookie in cookies:
764
+ for match in pattern.finditer(str(cookie.__dict__)):
765
+ included_cookies.append(cookie)
766
+ logger.debug(
767
+ "Loaded cookie for matching pattern '%s' => (%s: %s)",
768
+ pattern.pattern,
769
+ cookie.name,
770
+ cookie.value,
771
+ )
772
+ break
773
+ await connection.send(cdp.storage.set_cookies(included_cookies))
774
+
775
+ async def clear(self):
776
+ """
777
+ Clear current cookies.
778
+ Note: This includes all open tabs/windows for this browser.
779
+ """
780
+ connection = None
781
+ for _tab in self._browser.tabs:
782
+ if _tab.closed:
783
+ continue
784
+ connection = _tab
785
+ break
786
+ else:
787
+ connection = self._browser.connection
788
+ cookies = await connection.send(cdp.storage.get_cookies())
789
+ if cookies:
790
+ await connection.send(cdp.storage.clear_cookies())
791
+
792
+
793
+ class HTTPApi:
794
+ def __init__(self, addr: Tuple[str, int]):
795
+ self.host, self.port = addr
796
+ self.api = "http://%s:%d" % (self.host, self.port)
797
+
798
+ @classmethod
799
+ def from_target(cls, target):
800
+ ws_url = urllib.parse.urlparse(target.websocket_url)
801
+ inst = cls((ws_url.hostname, ws_url.port))
802
+ return inst
803
+
804
+ async def get(self, endpoint: str):
805
+ return await self._request(endpoint)
806
+
807
+ async def post(self, endpoint, data):
808
+ return await self._request(endpoint, data)
809
+
810
+ async def _request(self, endpoint, method: str = "get", data: dict = None):
811
+ url = urllib.parse.urljoin(
812
+ self.api, f"json/{endpoint}" if endpoint else "/json"
813
+ )
814
+ if data and method.lower() == "get":
815
+ raise ValueError("get requests cannot contain data")
816
+ if not url:
817
+ url = self.api + endpoint
818
+ request = urllib.request.Request(url)
819
+ request.method = method
820
+ request.data = None
821
+ if data:
822
+ request.data = json.dumps(data).encode("utf-8")
823
+
824
+ response = await asyncio.get_running_loop().run_in_executor(
825
+ None, lambda: urllib.request.urlopen(request, timeout=10)
826
+ )
827
+ return json.loads(response.read())
828
+
829
+
830
+ atexit.register(deconstruct_browser)