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.
- sbase/__init__.py +1 -0
- sbase/steps.py +7 -0
- seleniumbase/__init__.py +16 -7
- seleniumbase/__version__.py +1 -1
- seleniumbase/behave/behave_sb.py +97 -32
- seleniumbase/common/decorators.py +16 -7
- seleniumbase/config/proxy_list.py +3 -3
- seleniumbase/config/settings.py +4 -0
- seleniumbase/console_scripts/logo_helper.py +47 -8
- seleniumbase/console_scripts/run.py +345 -335
- seleniumbase/console_scripts/sb_behave_gui.py +5 -12
- seleniumbase/console_scripts/sb_caseplans.py +6 -13
- seleniumbase/console_scripts/sb_commander.py +5 -12
- seleniumbase/console_scripts/sb_install.py +62 -54
- seleniumbase/console_scripts/sb_mkchart.py +13 -20
- seleniumbase/console_scripts/sb_mkdir.py +11 -17
- seleniumbase/console_scripts/sb_mkfile.py +69 -43
- seleniumbase/console_scripts/sb_mkpres.py +13 -20
- seleniumbase/console_scripts/sb_mkrec.py +88 -21
- seleniumbase/console_scripts/sb_objectify.py +30 -30
- seleniumbase/console_scripts/sb_print.py +5 -12
- seleniumbase/console_scripts/sb_recorder.py +16 -11
- seleniumbase/core/browser_launcher.py +1658 -221
- seleniumbase/core/log_helper.py +42 -27
- seleniumbase/core/mysql.py +1 -4
- seleniumbase/core/proxy_helper.py +35 -30
- seleniumbase/core/recorder_helper.py +24 -5
- seleniumbase/core/sb_cdp.py +1951 -0
- seleniumbase/core/sb_driver.py +162 -8
- seleniumbase/core/settings_parser.py +6 -0
- seleniumbase/core/style_sheet.py +10 -0
- seleniumbase/extensions/recorder.zip +0 -0
- seleniumbase/fixtures/base_case.py +1225 -614
- seleniumbase/fixtures/constants.py +10 -1
- seleniumbase/fixtures/js_utils.py +171 -144
- seleniumbase/fixtures/page_actions.py +177 -13
- seleniumbase/fixtures/page_utils.py +25 -53
- seleniumbase/fixtures/shared_utils.py +97 -11
- seleniumbase/js_code/active_css_js.py +1 -1
- seleniumbase/js_code/recorder_js.py +1 -1
- seleniumbase/plugins/base_plugin.py +2 -3
- seleniumbase/plugins/driver_manager.py +340 -65
- seleniumbase/plugins/pytest_plugin.py +276 -47
- seleniumbase/plugins/sb_manager.py +412 -99
- seleniumbase/plugins/selenium_plugin.py +122 -17
- seleniumbase/translate/translator.py +0 -7
- seleniumbase/undetected/__init__.py +59 -52
- seleniumbase/undetected/cdp.py +0 -1
- seleniumbase/undetected/cdp_driver/__init__.py +1 -0
- seleniumbase/undetected/cdp_driver/_contradict.py +110 -0
- seleniumbase/undetected/cdp_driver/browser.py +829 -0
- seleniumbase/undetected/cdp_driver/cdp_util.py +458 -0
- seleniumbase/undetected/cdp_driver/config.py +334 -0
- seleniumbase/undetected/cdp_driver/connection.py +639 -0
- seleniumbase/undetected/cdp_driver/element.py +1168 -0
- seleniumbase/undetected/cdp_driver/tab.py +1323 -0
- seleniumbase/undetected/dprocess.py +4 -7
- seleniumbase/undetected/options.py +6 -8
- seleniumbase/undetected/patcher.py +11 -13
- seleniumbase/undetected/reactor.py +0 -1
- seleniumbase/undetected/webelement.py +16 -3
- {seleniumbase-4.24.11.dist-info → seleniumbase-4.33.15.dist-info}/LICENSE +1 -1
- {seleniumbase-4.24.11.dist-info → seleniumbase-4.33.15.dist-info}/METADATA +299 -252
- {seleniumbase-4.24.11.dist-info → seleniumbase-4.33.15.dist-info}/RECORD +67 -69
- {seleniumbase-4.24.11.dist-info → seleniumbase-4.33.15.dist-info}/WHEEL +1 -1
- sbase/ReadMe.txt +0 -2
- seleniumbase/ReadMe.md +0 -25
- seleniumbase/common/ReadMe.md +0 -71
- seleniumbase/console_scripts/ReadMe.md +0 -731
- seleniumbase/drivers/ReadMe.md +0 -27
- seleniumbase/extensions/ReadMe.md +0 -12
- seleniumbase/masterqa/ReadMe.md +0 -61
- seleniumbase/resources/ReadMe.md +0 -31
- seleniumbase/resources/favicon.ico +0 -0
- seleniumbase/utilities/selenium_grid/ReadMe.md +0 -84
- seleniumbase/utilities/selenium_ide/ReadMe.md +0 -111
- {seleniumbase-4.24.11.dist-info → seleniumbase-4.33.15.dist-info}/entry_points.txt +0 -0
- {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)
|