seleniumbase 4.24.11__py3-none-any.whl → 4.33.15__py3-none-any.whl

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