seleniumbase 4.24.10__py3-none-any.whl → 4.33.15__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.
Files changed (79) 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/detect_b_ver.py +7 -8
  25. seleniumbase/core/log_helper.py +42 -27
  26. seleniumbase/core/mysql.py +1 -4
  27. seleniumbase/core/proxy_helper.py +35 -30
  28. seleniumbase/core/recorder_helper.py +24 -5
  29. seleniumbase/core/sb_cdp.py +1951 -0
  30. seleniumbase/core/sb_driver.py +162 -8
  31. seleniumbase/core/settings_parser.py +6 -0
  32. seleniumbase/core/style_sheet.py +10 -0
  33. seleniumbase/extensions/recorder.zip +0 -0
  34. seleniumbase/fixtures/base_case.py +1234 -632
  35. seleniumbase/fixtures/constants.py +10 -1
  36. seleniumbase/fixtures/js_utils.py +171 -144
  37. seleniumbase/fixtures/page_actions.py +177 -13
  38. seleniumbase/fixtures/page_utils.py +25 -53
  39. seleniumbase/fixtures/shared_utils.py +97 -11
  40. seleniumbase/js_code/active_css_js.py +1 -1
  41. seleniumbase/js_code/recorder_js.py +1 -1
  42. seleniumbase/plugins/base_plugin.py +2 -3
  43. seleniumbase/plugins/driver_manager.py +340 -65
  44. seleniumbase/plugins/pytest_plugin.py +276 -47
  45. seleniumbase/plugins/sb_manager.py +412 -99
  46. seleniumbase/plugins/selenium_plugin.py +122 -17
  47. seleniumbase/translate/translator.py +0 -7
  48. seleniumbase/undetected/__init__.py +59 -52
  49. seleniumbase/undetected/cdp.py +0 -1
  50. seleniumbase/undetected/cdp_driver/__init__.py +1 -0
  51. seleniumbase/undetected/cdp_driver/_contradict.py +110 -0
  52. seleniumbase/undetected/cdp_driver/browser.py +829 -0
  53. seleniumbase/undetected/cdp_driver/cdp_util.py +458 -0
  54. seleniumbase/undetected/cdp_driver/config.py +334 -0
  55. seleniumbase/undetected/cdp_driver/connection.py +639 -0
  56. seleniumbase/undetected/cdp_driver/element.py +1168 -0
  57. seleniumbase/undetected/cdp_driver/tab.py +1323 -0
  58. seleniumbase/undetected/dprocess.py +4 -7
  59. seleniumbase/undetected/options.py +6 -8
  60. seleniumbase/undetected/patcher.py +11 -13
  61. seleniumbase/undetected/reactor.py +0 -1
  62. seleniumbase/undetected/webelement.py +16 -3
  63. {seleniumbase-4.24.10.dist-info → seleniumbase-4.33.15.dist-info}/LICENSE +1 -1
  64. {seleniumbase-4.24.10.dist-info → seleniumbase-4.33.15.dist-info}/METADATA +299 -252
  65. {seleniumbase-4.24.10.dist-info → seleniumbase-4.33.15.dist-info}/RECORD +68 -70
  66. {seleniumbase-4.24.10.dist-info → seleniumbase-4.33.15.dist-info}/WHEEL +1 -1
  67. sbase/ReadMe.txt +0 -2
  68. seleniumbase/ReadMe.md +0 -25
  69. seleniumbase/common/ReadMe.md +0 -71
  70. seleniumbase/console_scripts/ReadMe.md +0 -731
  71. seleniumbase/drivers/ReadMe.md +0 -27
  72. seleniumbase/extensions/ReadMe.md +0 -12
  73. seleniumbase/masterqa/ReadMe.md +0 -61
  74. seleniumbase/resources/ReadMe.md +0 -31
  75. seleniumbase/resources/favicon.ico +0 -0
  76. seleniumbase/utilities/selenium_grid/ReadMe.md +0 -84
  77. seleniumbase/utilities/selenium_ide/ReadMe.md +0 -111
  78. {seleniumbase-4.24.10.dist-info → seleniumbase-4.33.15.dist-info}/entry_points.txt +0 -0
  79. {seleniumbase-4.24.10.dist-info → seleniumbase-4.33.15.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,639 @@
1
+ from __future__ import annotations
2
+ import asyncio
3
+ import collections
4
+ import inspect
5
+ import itertools
6
+ import json
7
+ import logging
8
+ import sys
9
+ import types
10
+ from asyncio import iscoroutine, iscoroutinefunction
11
+ from typing import (
12
+ Optional,
13
+ Generator,
14
+ Union,
15
+ Awaitable,
16
+ Callable,
17
+ Any,
18
+ TypeVar,
19
+ )
20
+ import websockets
21
+ from websockets.protocol import State
22
+ from . import cdp_util as util
23
+ import mycdp as cdp
24
+ import mycdp.network
25
+ import mycdp.page
26
+ import mycdp.storage
27
+ import mycdp.runtime
28
+ import mycdp.target
29
+ import mycdp.util
30
+
31
+ T = TypeVar("T")
32
+ GLOBAL_DELAY = 0.005
33
+ MAX_SIZE: int = 2**28
34
+ PING_TIMEOUT: int = 1800 # 30 minutes
35
+ TargetType = Union[cdp.target.TargetInfo, cdp.target.TargetID]
36
+ logger = logging.getLogger("uc.connection")
37
+
38
+
39
+ class ProtocolException(Exception):
40
+ def __init__(self, *args, **kwargs):
41
+ self.message = None
42
+ self.code = None
43
+ self.args = args
44
+ if isinstance(args[0], dict):
45
+ self.message = args[0].get("message", None) # noqa
46
+ self.code = args[0].get("code", None)
47
+ elif hasattr(args[0], "to_json"):
48
+ def serialize(obj, _d=0):
49
+ res = "\n"
50
+ for k, v in obj.items():
51
+ space = "\t" * _d
52
+ if isinstance(v, dict):
53
+ res += f"{space}{k}: {serialize(v, _d + 1)}\n"
54
+ else:
55
+ res += f"{space}{k}: {v}\n"
56
+ return res
57
+ self.message = serialize(args[0].to_json())
58
+ else:
59
+ self.message = "| ".join(str(x) for x in args)
60
+
61
+ def __str__(self):
62
+ return f"{self.message} [code: {self.code}]" if self.code else f"{self.message}" # noqa
63
+
64
+
65
+ class SettingClassVarNotAllowedException(PermissionError):
66
+ pass
67
+
68
+
69
+ class Transaction(asyncio.Future):
70
+ __cdp_obj__: Generator = None
71
+ method: str = None
72
+ params: dict = None
73
+ id: int = None
74
+
75
+ def __init__(self, cdp_obj: Generator):
76
+ """
77
+ :param cdp_obj:
78
+ """
79
+ super().__init__()
80
+ self.__cdp_obj__ = cdp_obj
81
+ self.connection = None
82
+ self.method, *params = next(self.__cdp_obj__).values()
83
+ if params:
84
+ params = params.pop()
85
+ self.params = params
86
+
87
+ @property
88
+ def message(self):
89
+ return json.dumps(
90
+ {"method": self.method, "params": self.params, "id": self.id}
91
+ )
92
+
93
+ @property
94
+ def has_exception(self):
95
+ try:
96
+ if self.exception():
97
+ return True
98
+ except BaseException:
99
+ return True
100
+ return False
101
+
102
+ def __call__(self, **response: dict):
103
+ """
104
+ Parses the response message and marks the future complete.
105
+ :param response:
106
+ """
107
+ if "error" in response:
108
+ # Set exception and bail out
109
+ return self.set_exception(ProtocolException(response["error"]))
110
+ try:
111
+ # Try to parse the result according to the PyCDP docs.
112
+ self.__cdp_obj__.send(response["result"])
113
+ except StopIteration as e:
114
+ # Exception value holds the parsed response
115
+ return self.set_result(e.value)
116
+ raise ProtocolException(
117
+ "Could not parse the cdp response:\n%s" % response
118
+ )
119
+
120
+ def __repr__(self):
121
+ success = False if (self.done() and self.has_exception) else True
122
+ if self.done():
123
+ status = "finished"
124
+ else:
125
+ status = "pending"
126
+ fmt = (
127
+ f"<{self.__class__.__name__}\n\t"
128
+ f"method: {self.method}\n\t"
129
+ f"status: {status}\n\t"
130
+ f"success: {success}>"
131
+ )
132
+ return fmt
133
+
134
+
135
+ class EventTransaction(Transaction):
136
+ event = None
137
+ value = None
138
+
139
+ def __init__(self, event_object):
140
+ try:
141
+ super().__init__(None)
142
+ except BaseException:
143
+ pass
144
+ self.set_result(event_object)
145
+ self.event = self.value = self.result()
146
+
147
+ def __repr__(self):
148
+ status = "finished"
149
+ success = False if self.exception() else True
150
+ event_object = self.result()
151
+ fmt = (
152
+ f"{self.__class__.__name__}\n\t"
153
+ f"event: {event_object.__class__.__module__}.{event_object.__class__.__name__}\n\t" # noqa
154
+ f"status: {status}\n\t"
155
+ f"success: {success}>"
156
+ )
157
+ return fmt
158
+
159
+
160
+ class CantTouchThis(type):
161
+ def __setattr__(cls, attr, value):
162
+ """:meta private:"""
163
+ if attr == "__annotations__":
164
+ # Fix autodoc
165
+ return super().__setattr__(attr, value)
166
+ raise SettingClassVarNotAllowedException(
167
+ "\n".join(
168
+ (
169
+ "don't set '%s' on the %s class directly, "
170
+ "as those are shared with other objects.",
171
+ "use `my_object.%s = %s` instead",
172
+ )
173
+ )
174
+ % (attr, cls.__name__, attr, value)
175
+ )
176
+
177
+
178
+ class Connection(metaclass=CantTouchThis):
179
+ attached: bool = None
180
+ websocket: websockets.WebSocketClientProtocol
181
+ _target: cdp.target.TargetInfo
182
+
183
+ def __init__(
184
+ self,
185
+ websocket_url=None,
186
+ target=None,
187
+ _owner=None,
188
+ **kwargs,
189
+ ):
190
+ super().__init__()
191
+ self._target = target
192
+ self.__count__ = itertools.count(0)
193
+ self._owner = _owner
194
+ self.websocket_url: str = websocket_url
195
+ self.websocket = None
196
+ self.mapper = {}
197
+ self.handlers = collections.defaultdict(list)
198
+ self.recv_task = None
199
+ self.enabled_domains = []
200
+ self._last_result = []
201
+ self.listener: Listener = None
202
+ self.__dict__.update(**kwargs)
203
+
204
+ @property
205
+ def target(self) -> cdp.target.TargetInfo:
206
+ return self._target
207
+
208
+ @target.setter
209
+ def target(self, target: cdp.target.TargetInfo):
210
+ if not isinstance(target, cdp.target.TargetInfo):
211
+ raise TypeError(
212
+ "target must be set to a '%s' but got '%s"
213
+ % (cdp.target.TargetInfo.__name__, type(target).__name__)
214
+ )
215
+ self._target = target
216
+
217
+ @property
218
+ def closed(self):
219
+ if not self.websocket:
220
+ return True
221
+ return self.websocket.closed
222
+
223
+ def add_handler(
224
+ self,
225
+ event_type_or_domain: Union[type, types.ModuleType],
226
+ handler: Union[Callable, Awaitable],
227
+ ):
228
+ """
229
+ Add a handler for given event.
230
+ If event_type_or_domain is a module instead of a type,
231
+ it will find all available events and add the handler.
232
+ If you want to receive event updates (eg. network traffic),
233
+ you can add handlers for those events.
234
+ Handlers can be regular callback functions
235
+ or async coroutine functions (and also just lambdas).
236
+ For example, if you want to check the network traffic:
237
+ .. code-block::
238
+ page.add_handler(
239
+ cdp.network.RequestWillBeSent, lambda event: print(
240
+ 'network event => %s' % event.request
241
+ )
242
+ )
243
+ Next time there's network traffic, you'll see lots of console output.
244
+ :param event_type_or_domain:
245
+ :param handler:
246
+ """
247
+ if isinstance(event_type_or_domain, types.ModuleType):
248
+ for name, obj in inspect.getmembers_static(event_type_or_domain):
249
+ if name.isupper():
250
+ continue
251
+ if not name[0].isupper():
252
+ continue
253
+ if not isinstance(obj, type):
254
+ continue
255
+ if inspect.isbuiltin(obj):
256
+ continue
257
+ self.handlers[obj].append(handler)
258
+ return
259
+ self.handlers[event_type_or_domain].append(handler)
260
+
261
+ async def aopen(self, **kw):
262
+ """
263
+ Opens the websocket connection. Shouldn't be called manually by users.
264
+ """
265
+ if not self.websocket or self.websocket.state is State.CLOSED:
266
+ try:
267
+ self.websocket = await websockets.connect(
268
+ self.websocket_url,
269
+ ping_timeout=PING_TIMEOUT,
270
+ max_size=MAX_SIZE,
271
+ )
272
+ self.listener = Listener(self)
273
+ except (Exception,) as e:
274
+ logger.debug("Exception during opening of websocket: %s", e)
275
+ if self.listener:
276
+ self.listener.cancel()
277
+ raise
278
+ if not self.listener or not self.listener.running:
279
+ self.listener = Listener(self)
280
+ logger.debug(
281
+ "\n✅ Opened websocket connection to %s", self.websocket_url
282
+ )
283
+ # When a websocket connection is closed (either by error or on purpose)
284
+ # and reconnected, the registered event listeners (if any), should be
285
+ # registered again, so the browser sends those events.
286
+ await self._register_handlers()
287
+
288
+ async def aclose(self):
289
+ """
290
+ Closes the websocket connection. Shouldn't be called manually by users.
291
+ """
292
+ if self.websocket and self.websocket.state is not State.CLOSED:
293
+ if self.listener and self.listener.running:
294
+ self.listener.cancel()
295
+ self.enabled_domains.clear()
296
+ await asyncio.sleep(0.015)
297
+ try:
298
+ await self.websocket.close()
299
+ except Exception:
300
+ logger.debug(
301
+ "\n❌ Error closing websocket connection to %s",
302
+ self.websocket_url
303
+ )
304
+ logger.debug(
305
+ "\n❌ Closed websocket connection to %s", self.websocket_url
306
+ )
307
+
308
+ async def sleep(self, t: Union[int, float] = 0.25):
309
+ await self.update_target()
310
+ await asyncio.sleep(t)
311
+
312
+ def feed_cdp(self, cdp_obj):
313
+ """
314
+ Used in specific cases, mostly during cdp.fetch.RequestPaused events,
315
+ in which the browser literally blocks.
316
+ By using feed_cdp, you can issue a response without a blocking "await".
317
+ Note: This method won't cause a response.
318
+ Note: This is not an async method, just a regular method!
319
+ :param cdp_obj:
320
+ """
321
+ asyncio.ensure_future(self.send(cdp_obj))
322
+
323
+ async def wait(self, t: Union[int, float] = None):
324
+ """
325
+ Waits until the event listener reports idle
326
+ (no new events received in certain timespan).
327
+ When `t` is provided, ensures waiting for `t` seconds, no matter what.
328
+ :param t:
329
+ """
330
+ await self.update_target()
331
+ loop = asyncio.get_running_loop()
332
+ start_time = loop.time()
333
+ try:
334
+ if isinstance(t, (int, float)):
335
+ await asyncio.wait_for(self.listener.idle.wait(), timeout=t)
336
+ while (loop.time() - start_time) < t:
337
+ await asyncio.sleep(0.1)
338
+ else:
339
+ await self.listener.idle.wait()
340
+ except asyncio.TimeoutError:
341
+ if isinstance(t, (int, float)):
342
+ # Explicit time is given, which is now passed, so leave now.
343
+ return
344
+ except AttributeError:
345
+ # No listener created yet.
346
+ pass
347
+
348
+ async def set_locale(self, locale: Optional[str] = None):
349
+ """Sets the Language Locale code via set_user_agent_override."""
350
+ await self.send(cdp.network.set_user_agent_override("", locale))
351
+
352
+ def __getattr__(self, item):
353
+ """:meta private:"""
354
+ try:
355
+ return getattr(self.target, item)
356
+ except AttributeError:
357
+ raise
358
+
359
+ async def __aenter__(self):
360
+ """:meta private:"""
361
+ return self
362
+
363
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
364
+ """:meta private:"""
365
+ await self.aclose()
366
+ if exc_type and exc_val:
367
+ raise exc_type(exc_val)
368
+
369
+ def __await__(self):
370
+ """
371
+ Updates targets and wait for event listener to report idle.
372
+ Idle is reported when no new events are received for 1 second.
373
+ """
374
+ return self.wait().__await__()
375
+
376
+ async def update_target(self):
377
+ target_info: cdp.target.TargetInfo = await self.send(
378
+ cdp.target.get_target_info(self.target_id), _is_update=True
379
+ )
380
+ self.target = target_info
381
+
382
+ async def send(
383
+ self,
384
+ cdp_obj: Generator[dict[str, Any], dict[str, Any], Any],
385
+ _is_update=False,
386
+ ) -> Any:
387
+ """
388
+ Send a protocol command.
389
+ The commands are made using any of the cdp.<domain>.<method>()'s
390
+ and is used to send custom cdp commands as well.
391
+ :param cdp_obj: The generator object created by a cdp method
392
+ :param _is_update: Internal flag
393
+ Prevents infinite loop by skipping the registeration of handlers
394
+ when multiple calls to connection.send() are made.
395
+ """
396
+ await self.aopen()
397
+ if not self.websocket or self.websocket.state is State.CLOSED:
398
+ return
399
+ if self._owner:
400
+ browser = self._owner
401
+ if browser.config:
402
+ if browser.config.expert:
403
+ await self._prepare_expert()
404
+ if browser.config.headless:
405
+ await self._prepare_headless()
406
+ if not self.listener or not self.listener.running:
407
+ self.listener = Listener(self)
408
+ try:
409
+ tx = Transaction(cdp_obj)
410
+ tx.connection = self
411
+ if not self.mapper:
412
+ self.__count__ = itertools.count(0)
413
+ tx.id = next(self.__count__)
414
+ self.mapper.update({tx.id: tx})
415
+ if not _is_update:
416
+ await self._register_handlers()
417
+ await self.websocket.send(tx.message)
418
+ try:
419
+ return await tx
420
+ except ProtocolException as e:
421
+ e.message += f"\ncommand:{tx.method}\nparams:{tx.params}"
422
+ raise e
423
+ except Exception:
424
+ await self.aclose()
425
+
426
+ async def _register_handlers(self):
427
+ """
428
+ Ensure that for current (event) handlers, the corresponding
429
+ domain is enabled in the protocol.
430
+ """
431
+ # Save a copy of current enabled domains in a variable.
432
+ # At the end, this variable will hold the domains that
433
+ # are not represented by handlers, and can be removed.
434
+ enabled_domains = self.enabled_domains.copy()
435
+ for event_type in self.handlers.copy():
436
+ domain_mod = None
437
+ if len(self.handlers[event_type]) == 0:
438
+ self.handlers.pop(event_type)
439
+ continue
440
+ if isinstance(event_type, type):
441
+ domain_mod = util.cdp_get_module(event_type.__module__)
442
+ if domain_mod in self.enabled_domains:
443
+ # At this point, the domain is being used by a handler, so
444
+ # remove that domain from temp variable 'enabled_domains'.
445
+ if domain_mod in enabled_domains:
446
+ enabled_domains.remove(domain_mod)
447
+ continue
448
+ elif domain_mod not in self.enabled_domains:
449
+ if domain_mod in (cdp.target, cdp.storage):
450
+ continue
451
+ try:
452
+ # Prevent infinite loops.
453
+ logger.debug("Registered %s", domain_mod)
454
+ self.enabled_domains.append(domain_mod)
455
+ await self.send(domain_mod.enable(), _is_update=True)
456
+ except BaseException: # Don't error before request is sent
457
+ logger.debug("", exc_info=True)
458
+ try:
459
+ self.enabled_domains.remove(domain_mod)
460
+ except BaseException:
461
+ logger.debug("NOT GOOD", exc_info=True)
462
+ continue
463
+ finally:
464
+ continue
465
+ for ed in enabled_domains:
466
+ # Items still present at this point are unused and need removal.
467
+ self.enabled_domains.remove(ed)
468
+
469
+ async def _prepare_headless(self):
470
+ return # (This functionality has moved to a new location!)
471
+
472
+ async def _prepare_expert(self):
473
+ if getattr(self, "_prep_expert_done", None):
474
+ return
475
+ if self._owner:
476
+ part1 = "Element.prototype._attachShadow = "
477
+ part2 = "Element.prototype.attachShadow"
478
+ parts = part1 + part2
479
+ await self._send_oneshot(
480
+ cdp.page.add_script_to_evaluate_on_new_document(
481
+ """
482
+ %s;
483
+ Element.prototype.attachShadow = function () {
484
+ return this._attachShadow( { mode: "open" } );
485
+ };
486
+ """ % parts
487
+ )
488
+ )
489
+ await self._send_oneshot(cdp.page.enable())
490
+ setattr(self, "_prep_expert_done", True)
491
+
492
+ async def _send_oneshot(self, cdp_obj):
493
+ tx = Transaction(cdp_obj)
494
+ tx.connection = self
495
+ tx.id = -2
496
+ self.mapper.update({tx.id: tx})
497
+ await self.websocket.send(tx.message)
498
+ try:
499
+ # In try/except since if browser connection sends this,
500
+ # then it raises an exception.
501
+ return await tx
502
+ except ProtocolException:
503
+ pass
504
+
505
+
506
+ class Listener:
507
+ def __init__(self, connection: Connection):
508
+ self.connection = connection
509
+ self.history = collections.deque()
510
+ self.max_history = 1000
511
+ self.task: asyncio.Future = None
512
+ is_interactive = getattr(sys, "ps1", sys.flags.interactive)
513
+ self._time_before_considered_idle = 0.10 if not is_interactive else 0.75 # noqa
514
+ self.idle = asyncio.Event()
515
+ self.run()
516
+
517
+ def run(self):
518
+ self.task = asyncio.create_task(self.listener_loop())
519
+
520
+ @property
521
+ def time_before_considered_idle(self):
522
+ return self._time_before_considered_idle
523
+
524
+ @time_before_considered_idle.setter
525
+ def time_before_considered_idle(self, seconds: Union[int, float]):
526
+ self._time_before_considered_idle = seconds
527
+
528
+ def cancel(self):
529
+ if self.task and not self.task.cancelled():
530
+ self.task.cancel()
531
+
532
+ @property
533
+ def running(self):
534
+ if not self.task:
535
+ return False
536
+ if self.task.done():
537
+ return False
538
+ return True
539
+
540
+ async def listener_loop(self):
541
+ while True:
542
+ try:
543
+ msg = await asyncio.wait_for(
544
+ self.connection.websocket.recv(),
545
+ self.time_before_considered_idle,
546
+ )
547
+ except asyncio.TimeoutError:
548
+ self.idle.set()
549
+ # Pause for a moment.
550
+ # await asyncio.sleep(self.time_before_considered_idle / 10)
551
+ await asyncio.sleep(0.015)
552
+ continue
553
+ except (Exception,) as e:
554
+ logger.debug(
555
+ "Connection listener exception "
556
+ "while reading websocket:\n%s", e
557
+ )
558
+ break
559
+ if not self.running:
560
+ # If we have been cancelled or otherwise stopped running,
561
+ # then break this loop.
562
+ break
563
+ self.idle.clear() # Not "idle" anymore.
564
+ message = json.loads(msg)
565
+ if "id" in message:
566
+ if message["id"] in self.connection.mapper:
567
+ tx = self.connection.mapper.pop(message["id"])
568
+ logger.debug(
569
+ "Got answer for %s (message_id:%d)", tx, message["id"]
570
+ )
571
+ tx(**message)
572
+ else:
573
+ if message["id"] == -2:
574
+ tx = self.connection.mapper.get(-2)
575
+ if tx:
576
+ tx(**message)
577
+ continue
578
+ else:
579
+ # Probably an event
580
+ try:
581
+ event = cdp.util.parse_json_event(message)
582
+ event_tx = EventTransaction(event)
583
+ if not self.connection.mapper:
584
+ self.connection.__count__ = itertools.count(0)
585
+ event_tx.id = next(self.connection.__count__)
586
+ self.connection.mapper[event_tx.id] = event_tx
587
+ except Exception as e:
588
+ logger.info(
589
+ "%s: %s during parsing of json from event : %s"
590
+ % (type(e).__name__, e.args, message),
591
+ exc_info=True,
592
+ )
593
+ continue
594
+ except KeyError as e:
595
+ logger.info("KeyError: %s" % e, exc_info=True)
596
+ continue
597
+ try:
598
+ if type(event) in self.connection.handlers:
599
+ callbacks = self.connection.handlers[type(event)]
600
+ else:
601
+ continue
602
+ if not len(callbacks):
603
+ continue
604
+ for callback in callbacks:
605
+ try:
606
+ if (
607
+ iscoroutinefunction(callback)
608
+ or iscoroutine(callback)
609
+ ):
610
+ try:
611
+ await callback(event, self.connection)
612
+ except TypeError:
613
+ await callback(event)
614
+ else:
615
+ try:
616
+ callback(event, self.connection)
617
+ except TypeError:
618
+ callback(event)
619
+ except Exception as e:
620
+ logger.warning(
621
+ "Exception in callback %s for event %s => %s",
622
+ callback,
623
+ event.__class__.__name__,
624
+ e,
625
+ exc_info=True,
626
+ )
627
+ raise
628
+ except asyncio.CancelledError:
629
+ break
630
+ except Exception:
631
+ raise
632
+ continue
633
+
634
+ def __repr__(self):
635
+ s_idle = "[idle]" if self.idle.is_set() else "[busy]"
636
+ s_cache_length = f"[cache size: {len(self.history)}]"
637
+ s_running = f"[running: {self.running}]"
638
+ s = f"{self.__class__.__name__} {s_running} {s_idle} {s_cache_length}>"
639
+ return s