mithwire 0.50.3__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 (76) hide show
  1. mithwire/__init__.py +32 -0
  2. mithwire/cdp/README.md +4 -0
  3. mithwire/cdp/__init__.py +6 -0
  4. mithwire/cdp/accessibility.py +668 -0
  5. mithwire/cdp/animation.py +494 -0
  6. mithwire/cdp/audits.py +1995 -0
  7. mithwire/cdp/autofill.py +292 -0
  8. mithwire/cdp/background_service.py +215 -0
  9. mithwire/cdp/bluetooth_emulation.py +626 -0
  10. mithwire/cdp/browser.py +821 -0
  11. mithwire/cdp/cache_storage.py +311 -0
  12. mithwire/cdp/cast.py +172 -0
  13. mithwire/cdp/console.py +107 -0
  14. mithwire/cdp/crash_report_context.py +55 -0
  15. mithwire/cdp/css.py +2750 -0
  16. mithwire/cdp/database.py +179 -0
  17. mithwire/cdp/debugger.py +1405 -0
  18. mithwire/cdp/device_access.py +141 -0
  19. mithwire/cdp/device_orientation.py +45 -0
  20. mithwire/cdp/dom.py +2257 -0
  21. mithwire/cdp/dom_debugger.py +321 -0
  22. mithwire/cdp/dom_snapshot.py +876 -0
  23. mithwire/cdp/dom_storage.py +222 -0
  24. mithwire/cdp/emulation.py +1779 -0
  25. mithwire/cdp/event_breakpoints.py +56 -0
  26. mithwire/cdp/extensions.py +238 -0
  27. mithwire/cdp/fed_cm.py +283 -0
  28. mithwire/cdp/fetch.py +507 -0
  29. mithwire/cdp/file_system.py +115 -0
  30. mithwire/cdp/headless_experimental.py +115 -0
  31. mithwire/cdp/heap_profiler.py +401 -0
  32. mithwire/cdp/indexed_db.py +528 -0
  33. mithwire/cdp/input_.py +701 -0
  34. mithwire/cdp/inspector.py +95 -0
  35. mithwire/cdp/io.py +101 -0
  36. mithwire/cdp/layer_tree.py +464 -0
  37. mithwire/cdp/log.py +190 -0
  38. mithwire/cdp/media.py +313 -0
  39. mithwire/cdp/memory.py +305 -0
  40. mithwire/cdp/network.py +5342 -0
  41. mithwire/cdp/overlay.py +1468 -0
  42. mithwire/cdp/page.py +3972 -0
  43. mithwire/cdp/performance.py +124 -0
  44. mithwire/cdp/performance_timeline.py +200 -0
  45. mithwire/cdp/preload.py +575 -0
  46. mithwire/cdp/profiler.py +420 -0
  47. mithwire/cdp/pwa.py +278 -0
  48. mithwire/cdp/py.typed +0 -0
  49. mithwire/cdp/runtime.py +1589 -0
  50. mithwire/cdp/schema.py +50 -0
  51. mithwire/cdp/security.py +518 -0
  52. mithwire/cdp/service_worker.py +401 -0
  53. mithwire/cdp/smart_card_emulation.py +891 -0
  54. mithwire/cdp/storage.py +1573 -0
  55. mithwire/cdp/system_info.py +327 -0
  56. mithwire/cdp/target.py +829 -0
  57. mithwire/cdp/tethering.py +65 -0
  58. mithwire/cdp/tracing.py +377 -0
  59. mithwire/cdp/util.py +18 -0
  60. mithwire/cdp/web_audio.py +606 -0
  61. mithwire/cdp/web_authn.py +598 -0
  62. mithwire/cdp/web_mcp.py +293 -0
  63. mithwire/core/_contradict.py +142 -0
  64. mithwire/core/browser.py +923 -0
  65. mithwire/core/cf_templates/cf_dark_checkbox.png +0 -0
  66. mithwire/core/cf_templates/cf_light_checkbox.png +0 -0
  67. mithwire/core/config.py +323 -0
  68. mithwire/core/connection.py +564 -0
  69. mithwire/core/element.py +1205 -0
  70. mithwire/core/tab.py +2202 -0
  71. mithwire/core/util.py +5063 -0
  72. mithwire-0.50.3.dist-info/METADATA +1049 -0
  73. mithwire-0.50.3.dist-info/RECORD +76 -0
  74. mithwire-0.50.3.dist-info/WHEEL +5 -0
  75. mithwire-0.50.3.dist-info/licenses/LICENSE.txt +619 -0
  76. mithwire-0.50.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1205 @@
1
+ # Copyright 2024 by UltrafunkAmsterdam (https://github.com/UltrafunkAmsterdam)
2
+ # All rights reserved.
3
+ # This file is part of the mithwire package.
4
+ # and is released under the "GNU AFFERO GENERAL PUBLIC LICENSE".
5
+ # Please see the LICENSE.txt file that should have been included as part of this package.
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import logging
11
+ import pathlib
12
+ import secrets
13
+ import typing
14
+
15
+ from .. import cdp
16
+ from . import util
17
+ from ._contradict import ContraDict
18
+ from .config import PathLike
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ if typing.TYPE_CHECKING:
23
+ from .tab import Tab
24
+
25
+
26
+ def create(node: cdp.dom.Node, tab: Tab, tree: typing.Optional[cdp.dom.Node] = None):
27
+ """
28
+ factory for Elements
29
+ this is used with Tab.query_selector(_all), since we already have the tree,
30
+ we don't need to fetch it for every single element.
31
+
32
+ :param node: cdp dom node representation
33
+ :type node: cdp.dom.Node
34
+ :param tab: the target object to which this element belongs
35
+ :type tab: Tab
36
+ :param tree: [Optional] the full node tree to which <node> belongs, enhances performance.
37
+ when not provided, you need to call `await elem.update()` before using .children / .parent
38
+ :type tree:
39
+ """
40
+
41
+ elem = Element(node, tab, tree)
42
+
43
+ return elem
44
+
45
+
46
+ class Element:
47
+ def __init__(self, node: cdp.dom.Node, tab: Tab, tree: cdp.dom.Node = None):
48
+ """
49
+ Represents an (HTML) DOM Element
50
+
51
+ :param node: cdp dom node representation
52
+ :type node: cdp.dom.Node
53
+ :param tab: the target object to which this element belongs
54
+ :type tab: Tab
55
+ """
56
+ if not node:
57
+ raise Exception("node cannot be None")
58
+ self._tab = tab
59
+
60
+ self._node = node
61
+ self._tree = tree
62
+ self._parent = None
63
+ self._remote_object = None
64
+ self._attrs = ContraDict(silent=True)
65
+ self._make_attrs()
66
+
67
+ @property
68
+ def tag(self):
69
+ if self.node_name:
70
+ return self.node_name.lower()
71
+
72
+ @property
73
+ def tag_name(self):
74
+ return self.tag
75
+
76
+ @property
77
+ def node_id(self):
78
+ return self.node.node_id
79
+
80
+ @property
81
+ def backend_node_id(self):
82
+ return self.node.backend_node_id
83
+
84
+ @property
85
+ def node_type(self):
86
+ return self.node.node_type
87
+
88
+ @property
89
+ def node_name(self):
90
+ return self.node.node_name
91
+
92
+ @property
93
+ def local_name(self):
94
+ return self.node.local_name
95
+
96
+ @property
97
+ def node_value(self):
98
+ return self.node.node_value
99
+
100
+ @property
101
+ def parent_id(self):
102
+ return self.node.parent_id
103
+
104
+ @property
105
+ def child_node_count(self):
106
+ return self.node.child_node_count
107
+
108
+ @property
109
+ def attributes(self):
110
+ return self.node.attributes
111
+
112
+ @property
113
+ def document_url(self):
114
+ return self.node.document_url
115
+
116
+ @property
117
+ def base_url(self):
118
+ return self.node.base_url
119
+
120
+ @property
121
+ def public_id(self):
122
+ return self.node.public_id
123
+
124
+ @property
125
+ def system_id(self):
126
+ return self.node.system_id
127
+
128
+ @property
129
+ def internal_subset(self):
130
+ return self.node.internal_subset
131
+
132
+ @property
133
+ def xml_version(self):
134
+ return self.node.xml_version
135
+
136
+ @property
137
+ def value(self):
138
+ return self.node.value
139
+
140
+ @property
141
+ def pseudo_type(self):
142
+ return self.node.pseudo_type
143
+
144
+ @property
145
+ def pseudo_identifier(self):
146
+ return self.node.pseudo_identifier
147
+
148
+ @property
149
+ def shadow_root_type(self):
150
+ return self.node.shadow_root_type
151
+
152
+ @property
153
+ def frame_id(self):
154
+ return self.node.frame_id
155
+
156
+ @property
157
+ def content_document(self):
158
+ return self.node.content_document
159
+
160
+ @property
161
+ def shadow_roots(self):
162
+ return self.node.shadow_roots
163
+
164
+ @property
165
+ def template_content(self):
166
+ return self.node.template_content
167
+
168
+ @property
169
+ def pseudo_elements(self):
170
+ return self.node.pseudo_elements
171
+
172
+ @property
173
+ def imported_document(self):
174
+ return self.node.imported_document
175
+
176
+ @property
177
+ def distributed_nodes(self):
178
+ return self.node.distributed_nodes
179
+
180
+ @property
181
+ def is_svg(self):
182
+ return self.node.is_svg
183
+
184
+ @property
185
+ def compatibility_mode(self):
186
+ return self.node.compatibility_mode
187
+
188
+ @property
189
+ def assigned_slot(self):
190
+ return self.node.assigned_slot
191
+
192
+ @property
193
+ def tab(self):
194
+ return self._tab
195
+
196
+ @property
197
+ def shadow_children(self):
198
+ if self.shadow_roots:
199
+ root = self.shadow_roots[0]
200
+ if root.shadow_root_type == cdp.dom.ShadowRootType.OPEN_:
201
+ return [create(child, self.tab) for child in root.children]
202
+
203
+ def __getattr__(self, item):
204
+ # if attribute is not found on the element python object
205
+ # check if it may be present in the element attributes (eg, href=, src=, alt=)
206
+ # returns None when attribute is not found
207
+ # instead of raising AttributeError
208
+ x = getattr(self.attrs, item, None)
209
+ if x:
210
+ return x
211
+ else:
212
+ logger.debug("could not find attribute '%s' in %s" % (item, self.attrs))
213
+
214
+ def __setattr__(self, key, value):
215
+ if key[0] != "_":
216
+ if key[1:] not in vars(self).keys():
217
+ # we probably deal with an attribute of
218
+ # the html element, so forward it
219
+ self.attrs.__setattr__(key, value)
220
+ return
221
+ # we probably deal with an attribute of
222
+ # the python object
223
+ super().__setattr__(key, value)
224
+
225
+ def __setitem__(self, key, value):
226
+ if key[0] != "_":
227
+ if key[1:] not in vars(self).keys():
228
+ # we probably deal with an attribute of
229
+ # the html element, so forward it
230
+ self.attrs[key] = value
231
+
232
+ def __getitem__(self, item):
233
+ # we probably deal with an attribute of
234
+ # the html element, so forward it
235
+ return self.attrs.get(item, None)
236
+
237
+ async def save_to_dom(self):
238
+ """
239
+ saves element to dom
240
+ :return:
241
+ :rtype:
242
+ """
243
+ self._remote_object = await self._tab.send(
244
+ cdp.dom.resolve_node(backend_node_id=self.backend_node_id)
245
+ )
246
+ await self._tab.send(cdp.dom.set_outer_html(self.node_id, outer_html=str(self)))
247
+ await self.update()
248
+
249
+ async def remove_from_dom(self):
250
+ """removes the element from dom"""
251
+ await self.update() # ensure we have latest node_id
252
+ node = util.filter_recurse(
253
+ self._tree, lambda node: node.backend_node_id == self.backend_node_id
254
+ )
255
+ if node:
256
+ await self.tab.send(cdp.dom.remove_node(node.node_id))
257
+ # self._tree = util.remove_from_tree(self.tree, self.node)
258
+
259
+ async def update(self, _node=None):
260
+ """
261
+ updates element to retrieve more properties. for example this enables
262
+ :py:obj:`~children` and :py:obj:`~parent` attributes.
263
+
264
+ also resolves js opbject which is stored object in :py:obj:`~remote_object`
265
+
266
+ usually you will get element nodes by the usage of
267
+
268
+ :py:meth:`Tab.query_selector_all()`
269
+
270
+ :py:meth:`Tab.find_elements_by_text()`
271
+
272
+ those elements are already updated and you can browse through children directly.
273
+
274
+ The reason for a seperate call instead of doing it at initialization,
275
+ is because when you are retrieving 100+ elements this becomes quite expensive.
276
+
277
+ therefore, it is not advised to call this method on a bunch of blocks (100+) at the same time.
278
+
279
+ :return:
280
+ :rtype:
281
+ """
282
+ if _node:
283
+ doc = _node
284
+ self._parent = None
285
+ else:
286
+ doc = await self._tab.send(cdp.dom.get_document(-1, True))
287
+ self._parent = None
288
+ updated_node = util.filter_recurse(
289
+ doc, lambda n: n.backend_node_id == self._node.backend_node_id
290
+ )
291
+ if updated_node:
292
+ logger.debug("node seems changed, and has now been updated.")
293
+ self._node = updated_node
294
+ self._tree = doc
295
+
296
+ self._remote_object = await self._tab.send(
297
+ cdp.dom.resolve_node(backend_node_id=self._node.backend_node_id)
298
+ )
299
+ self.attrs.clear()
300
+ self._make_attrs()
301
+ if self.node_name != "IFRAME":
302
+ parent_node = util.filter_recurse(
303
+ doc, lambda n: n.node_id == self.node.parent_id
304
+ )
305
+ if not parent_node:
306
+ # could happen if node is for example <html>
307
+ return self
308
+ self._parent = create(parent_node, tab=self._tab, tree=self._tree)
309
+ return self
310
+
311
+ @property
312
+ def node(self):
313
+ return self._node
314
+
315
+ @property
316
+ def tree(self) -> cdp.dom.Node:
317
+ return self._tree
318
+ # raise RuntimeError("you should first call `await update()` on this object to populate it's tree")
319
+
320
+ @tree.setter
321
+ def tree(self, tree: cdp.dom.Node):
322
+ self._tree = tree
323
+
324
+ @property
325
+ def attrs(self):
326
+ """
327
+ attributes are stored here, however, you can set them directly on the element object as well.
328
+ :return:
329
+ :rtype:
330
+ """
331
+ return self._attrs
332
+
333
+ @property
334
+ def parent(self) -> typing.Union[Element, None]:
335
+ """
336
+ get the parent element (node) of current element(node)
337
+ :return:
338
+ :rtype:
339
+ """
340
+ if not self.tree:
341
+ raise RuntimeError("could not get parent since the element has no tree set")
342
+ parent_node = util.filter_recurse(
343
+ self.tree, lambda n: n.node_id == self.parent_id
344
+ )
345
+ if not parent_node:
346
+ return None
347
+ parent_element = create(parent_node, tab=self._tab, tree=self.tree)
348
+ return parent_element
349
+
350
+ @property
351
+ def children(self) -> typing.Union[typing.List[Element], str]:
352
+ """
353
+ returns the elements' children. those children also have a children property
354
+ so you can browse through the entire tree as well.
355
+ :return:
356
+ :rtype:
357
+ """
358
+ _children = []
359
+ if self._node.node_name == "IFRAME":
360
+ # iframes are not exact the same as other nodes
361
+ # the children of iframes are found under
362
+ # the .content_document property, which is of more
363
+ # use than the node itself
364
+ frame = self._node.content_document
365
+ if not frame.child_node_count:
366
+ return []
367
+ for child in frame.children:
368
+ child_elem = create(child, self._tab, frame)
369
+ if child_elem:
370
+ _children.append(child_elem)
371
+ # self._node = frame
372
+ return _children
373
+ elif not self.node.child_node_count:
374
+ return []
375
+ if self.node.children:
376
+ for child in self.node.children:
377
+ child_elem = create(child, self._tab, self.tree)
378
+ if child_elem:
379
+ _children.append(child_elem)
380
+ return _children
381
+
382
+ @property
383
+ def remote_object(self) -> cdp.runtime.RemoteObject:
384
+ return self._remote_object
385
+
386
+ @property
387
+ def object_id(self) -> cdp.runtime.RemoteObjectId:
388
+ try:
389
+ return self.remote_object.object_id
390
+ except AttributeError:
391
+ pass
392
+
393
+ async def click(self):
394
+ """
395
+ Click the element.
396
+
397
+ :return:
398
+ :rtype:
399
+ """
400
+ self._remote_object = await self._tab.send(
401
+ cdp.dom.resolve_node(backend_node_id=self.backend_node_id)
402
+ )
403
+ arguments = [cdp.runtime.CallArgument(object_id=self._remote_object.object_id)]
404
+ await self.flash(0.25)
405
+ await self._tab.send(
406
+ cdp.runtime.call_function_on(
407
+ "(el) => el.click()",
408
+ object_id=self._remote_object.object_id,
409
+ arguments=arguments,
410
+ await_promise=True,
411
+ user_gesture=True,
412
+ return_by_value=True,
413
+ )
414
+ )
415
+
416
+ async def get_js_attributes(self):
417
+ return ContraDict(
418
+ json.loads(
419
+ await self.apply(
420
+ """
421
+ function (e) {
422
+ let o = {}
423
+ for(let k in e){
424
+ o[k] = e[k]
425
+ }
426
+ return JSON.stringify(o)
427
+ }
428
+ """
429
+ )
430
+ )
431
+ )
432
+
433
+ def __await__(self):
434
+ return self.update().__await__()
435
+
436
+ def __call__(self, js_method):
437
+ """
438
+ calling the element object will call a js method on the object
439
+ eg, element.play() in case of a video element, it will call .play()
440
+ :param js_method:
441
+ :type js_method:
442
+ :return:
443
+ :rtype:
444
+ """
445
+ return self.apply(f"(e) => e['{js_method}']()")
446
+
447
+ async def apply(self, js_function, return_by_value=True):
448
+ """
449
+ apply javascript to this element. the given js_function string should accept the js element as parameter,
450
+ and can be a arrow function, or function declaration.
451
+ eg:
452
+ - '(elem) => { elem.value = "blabla"; consolelog(elem); alert(JSON.stringify(elem); } '
453
+ - 'elem => elem.play()'
454
+ - function myFunction(elem) { alert(elem) }
455
+
456
+ :param js_function: the js function definition which received this element.
457
+ :type js_function: str
458
+ :param return_by_value:
459
+ :type return_by_value:
460
+ :return:
461
+ :rtype:
462
+ """
463
+ self._remote_object = await self._tab.send(
464
+ cdp.dom.resolve_node(backend_node_id=self.backend_node_id)
465
+ )
466
+ result: typing.Tuple[cdp.runtime.RemoteObject, typing.Any] = (
467
+ await self._tab.send(
468
+ cdp.runtime.call_function_on(
469
+ js_function,
470
+ object_id=self._remote_object.object_id,
471
+ arguments=[
472
+ cdp.runtime.CallArgument(
473
+ object_id=self._remote_object.object_id
474
+ )
475
+ ],
476
+ return_by_value=True,
477
+ user_gesture=True,
478
+ )
479
+ )
480
+ )
481
+ if result and result[0]:
482
+ if return_by_value:
483
+ return result[0].value
484
+ return result[0]
485
+ elif result[1]:
486
+ return result[1]
487
+
488
+ async def get_position(self, abs=False) -> Position:
489
+ if not self.parent or not self.object_id:
490
+ self._remote_object = await self._tab.send(
491
+ cdp.dom.resolve_node(backend_node_id=self.backend_node_id)
492
+ )
493
+ # await self.update()
494
+ try:
495
+ quads = await self.tab.send(
496
+ cdp.dom.get_content_quads(object_id=self.remote_object.object_id)
497
+ )
498
+ if not quads:
499
+ raise Exception("could not find position for %s " % self)
500
+ pos = Position(quads[0])
501
+ if abs:
502
+ scroll_y = await self.tab.evaluate("window.scrollY")
503
+ scroll_x = await self.tab.evaluate("window.scrollX")
504
+ abs_x = pos.left + scroll_x + (pos.width / 2)
505
+ abs_y = pos.top + scroll_y + (pos.height / 2)
506
+ pos.abs_x = abs_x
507
+ pos.abs_y = abs_y
508
+ return pos
509
+ except IndexError:
510
+ logger.debug(
511
+ "no content quads for %s. mostly caused by element which is not 'in plain sight'"
512
+ % self
513
+ )
514
+
515
+ async def mouse_click(
516
+ self,
517
+ button: str = "left",
518
+ buttons: typing.Optional[int] = 1,
519
+ modifiers: typing.Optional[int] = 0,
520
+ _until_event: typing.Optional[type] = None,
521
+ ):
522
+ """native click (on element) . note: this likely does not work atm, use click() instead
523
+
524
+ :param button: str (default = "left")
525
+ :param buttons: which button (default 1 = left)
526
+ :param modifiers: *(Optional)* Bit field representing pressed modifier keys.
527
+ Alt=1, Ctrl=2, Meta/Command=4, Shift=8 (default: 0).
528
+ :param _until_event: internal. event to wait for before returning
529
+ :return:
530
+
531
+ """
532
+ try:
533
+ center = (await self.get_position()).center
534
+ except AttributeError:
535
+ return
536
+ if not center:
537
+ logger.warning("could not calculate box model for %s", self)
538
+ return
539
+
540
+ logger.debug("clicking on location %.2f, %.2f" % center)
541
+
542
+ await self._tab.mouse_click(center[0], center[1])
543
+ await self._tab.flash_point(center[0], center[1])
544
+
545
+ # await asyncio.gather(
546
+ # self._tab.send(
547
+ # cdp.input_.dispatch_mouse_event(
548
+ # "mousePressed",
549
+ # x=center[0],
550
+ # y=center[1],
551
+ # modifiers=modifiers,
552
+ # button=cdp.input_.MouseButton(button),
553
+ # buttons=buttons,
554
+ # click_count=1,
555
+ # )
556
+ # ),
557
+ # self._tab.send(
558
+ # cdp.input_.dispatch_mouse_event(
559
+ # "mouseReleased",
560
+ # x=center[0],
561
+ # y=center[1],
562
+ # modifiers=modifiers,
563
+ # button=cdp.input_.MouseButton(button),
564
+ # buttons=buttons,
565
+ # click_count=1,
566
+ # )
567
+ # ),
568
+ # )
569
+ # try:
570
+ # await self.flash()
571
+ # except: # noqa
572
+ # pass
573
+
574
+ click_mouse = mouse_click
575
+
576
+ async def mouse_move(self):
577
+ """moves mouse (not click), to element position. when an element has an
578
+ hover/mouseover effect, this would trigger it"""
579
+ try:
580
+ center = (await self.get_position()).center
581
+ except AttributeError:
582
+ logger.debug("did not find location for %s", self)
583
+ return
584
+ logger.debug(
585
+ "mouse move to location %.2f, %.2f where %s is located", *center, self
586
+ )
587
+ await self._tab.mouse_move(center[0], center[1])
588
+
589
+ # cdp.input_.dispatch_mouse_event("mouseMoved", x=center[0], y=center[1])
590
+ # )
591
+ # await self._tab.sleep(0.05)
592
+ # await self._tab.send(
593
+ # cdp.input_.dispatch_mouse_event("mouseReleased", x=center[0], y=center[1])
594
+ # )
595
+
596
+ async def mouse_drag(
597
+ self,
598
+ destination: typing.Union[Element, typing.Tuple[int, int]],
599
+ relative: bool = False,
600
+ steps: int = 1,
601
+ ):
602
+ """
603
+ drag an element to another element or target coordinates. dragging of elements should be supported by the site of course
604
+
605
+
606
+ :param destination: another element where to drag to, or a tuple (x,y) of ints representing coordinate
607
+ :type destination: Element or coordinate as x,y tuple
608
+
609
+ :param relative: when True, treats coordinate as relative. for example (-100, 200) will move left 100px and down 200px
610
+ :type relative:
611
+
612
+ :param steps: move in <steps> points, this could make it look more "natural" (default 1),
613
+ but also a lot slower.
614
+ for very smooth action use 50-100
615
+ :type steps: int
616
+ :return:
617
+ :rtype:
618
+ """
619
+ try:
620
+ start_point = (await self.get_position()).center
621
+ except AttributeError:
622
+ return
623
+ if not start_point:
624
+ logger.warning("could not calculate box model for %s", self)
625
+ return
626
+ end_point = None
627
+ if isinstance(destination, Element):
628
+ try:
629
+ end_point = (await destination.get_position()).center
630
+ except AttributeError:
631
+ return
632
+ if not end_point:
633
+ logger.warning("could not calculate box model for %s", destination)
634
+ return
635
+ elif isinstance(destination, (tuple, list)):
636
+ if relative:
637
+ end_point = (
638
+ start_point[0] + destination[0],
639
+ start_point[1] + destination[1],
640
+ )
641
+ else:
642
+ end_point = destination
643
+ await self._tab.mouse_drag(
644
+ start_point, end_point, relative=relative, steps=steps
645
+ )
646
+ # await self._tab.send(
647
+ # cdp.input_.dispatch_mouse_event(
648
+ # "mousePressed",
649
+ # x=start_point[0],
650
+ # y=start_point[1],
651
+ # button=cdp.input_.MouseButton("left"),
652
+ # )
653
+ # )
654
+ #
655
+ # steps = 1 if (not steps or steps < 1) else steps
656
+ # if steps == 1:
657
+ # await self._tab.send(
658
+ # cdp.input_.dispatch_mouse_event(
659
+ # "mouseMoved",
660
+ # x=end_point[0],
661
+ # y=end_point[1],
662
+ # )
663
+ # )
664
+ # elif steps > 1:
665
+ # # probably the worst waay of calculating this. but couldn't think of a better solution today.
666
+ # step_size_x = (end_point[0] - start_point[0]) / steps
667
+ # step_size_y = (end_point[1] - start_point[1]) / steps
668
+ # pathway = [
669
+ # (start_point[0] + step_size_x * i, start_point[1] + step_size_y * i)
670
+ # for i in range(steps + 1)
671
+ # ]
672
+ #
673
+ # for point in pathway:
674
+ # await self._tab.send(
675
+ # cdp.input_.dispatch_mouse_event(
676
+ # "mouseMoved",
677
+ # x=point[0],
678
+ # y=point[1],
679
+ # )
680
+ # )
681
+ # await asyncio.sleep(0)
682
+ #
683
+ # await self._tab.send(
684
+ # cdp.input_.dispatch_mouse_event(
685
+ # type_="mouseReleased",
686
+ # x=end_point[0],
687
+ # y=end_point[1],
688
+ # button=cdp.input_.MouseButton("left"),
689
+ # )
690
+ # )
691
+
692
+ async def scroll_into_view(self):
693
+ """scrolls element into view"""
694
+ try:
695
+ await self.tab.send(
696
+ cdp.dom.scroll_into_view_if_needed(backend_node_id=self.backend_node_id)
697
+ )
698
+ except Exception as e:
699
+ logger.debug("could not scroll into view: %s", e)
700
+ return
701
+
702
+ # await self.apply("""(el) => el.scrollIntoView(false)""")
703
+
704
+ async def clear_input(self, _until_event: type = None):
705
+ """clears an input field"""
706
+ return await self.apply('function (element) { element.value = "" } ')
707
+
708
+ async def send_keys(self, text: str):
709
+ """
710
+ send text to an input field, or any other html element.
711
+
712
+ hint, if you ever get stuck where using py:meth:`~click`
713
+ does not work, sending the keystroke \\n or \\r\\n or a spacebar work wonders!
714
+
715
+ :param text: text to send
716
+ :return: None
717
+ """
718
+ await self.apply("(elem) => elem.focus()")
719
+ [
720
+ await self._tab.send(cdp.input_.dispatch_key_event("char", text=char))
721
+ for char in list(text)
722
+ ]
723
+
724
+ async def send_file(self, *file_paths: PathLike):
725
+ """
726
+ some form input require a file (upload), a full path needs to be provided.
727
+ this method sends 1 or more file(s) to the input field.
728
+
729
+ needles to say, but make sure the field accepts multiple files if you want to send more files.
730
+ otherwise the browser might crash.
731
+
732
+ example :
733
+ `await fileinputElement.send_file('c:/temp/image.png', 'c:/users/myuser/lol.gif')`
734
+
735
+ """
736
+ file_paths = [str(p) for p in file_paths]
737
+ await self._tab.send(
738
+ cdp.dom.set_file_input_files(
739
+ files=[*file_paths],
740
+ backend_node_id=self.backend_node_id,
741
+ object_id=self.object_id,
742
+ )
743
+ )
744
+
745
+ async def focus(self):
746
+ """focus the current element. often useful in form (select) fields"""
747
+ return await self.apply("(element) => element.focus()")
748
+
749
+ async def select_option(self):
750
+ """
751
+ for form (select) fields. when you have queried the options you can call this method on the option object.
752
+ 02/08/2024: fixed the problem where events are not fired when programattically selecting an option.
753
+
754
+ calling :func:`option.select_option()` will use that option as selected value.
755
+ does not work in all cases.
756
+
757
+ """
758
+ if self.node_name == "OPTION":
759
+ await self.apply(
760
+ """
761
+ (o) => {
762
+ o.selected = true ;
763
+ o.dispatchEvent(new Event('change', {view: window,bubbles: true}))
764
+ }
765
+ """
766
+ )
767
+
768
+ async def set_value(self, value):
769
+ await self._tab.send(cdp.dom.set_node_value(node_id=self.node_id, value=value))
770
+
771
+ async def set_text(self, value):
772
+ if not self.node_type == 3:
773
+ if self.child_node_count == 1:
774
+ child_node = self.children[0]
775
+ await child_node.set_text(value)
776
+ await self.update()
777
+ return
778
+ else:
779
+ raise RuntimeError("could only set value of text nodes")
780
+ await self.update()
781
+ await self._tab.send(cdp.dom.set_node_value(node_id=self.node_id, value=value))
782
+
783
+ async def get_html(self):
784
+ return await self._tab.send(
785
+ cdp.dom.get_outer_html(backend_node_id=self.backend_node_id)
786
+ )
787
+
788
+ @property
789
+ def text(self) -> str:
790
+ """
791
+ gets the text contents of this element
792
+ note: this includes text in the form of script content, as those are also just 'text nodes'
793
+
794
+ :return:
795
+ :rtype:
796
+ """
797
+ text_node = util.filter_recurse(self.node, lambda n: n.node_type == 3)
798
+ if text_node:
799
+ return text_node.node_value
800
+ return ""
801
+
802
+ @property
803
+ def text_all(self):
804
+ """
805
+ gets the text contents of this element, and it's children in a concatenated string
806
+ note: this includes text in the form of script content, as those are also just 'text nodes'
807
+ :return:
808
+ :rtype:
809
+ """
810
+ text_nodes = util.filter_recurse_all(self.node, lambda n: n.node_type == 3)
811
+ return " ".join([n.node_value for n in text_nodes])
812
+
813
+ async def query_selector_all(self, selector: str):
814
+ """
815
+ like js querySelectorAll()
816
+ """
817
+ await self.update()
818
+ return await self.tab.query_selector_all(selector, _node=self)
819
+
820
+ async def query_selector(self, selector):
821
+ """
822
+ like js querySelector()
823
+ """
824
+
825
+ await self.update()
826
+ return await self.tab.query_selector(selector, self)
827
+
828
+ # async def find_all(self, string: str):
829
+ # base_node = self.node
830
+ # if self.node.node_name == "IFRAME":
831
+ # if self.node.content_document:
832
+ # base_node = self.node.content_document
833
+ # cdp.target.attach_to_target()
834
+ # cdp.target.create_target()
835
+ # cdp.target.create_browser_context()
836
+ # search_id, nresult = await self.tab.send(cdp.dom.perform_search(string, True))
837
+ # if nresult:
838
+ # node_ids = await self.send(
839
+ # cdp.dom.get_search_results(search_id, 0, nresult)
840
+ # )
841
+ # # doc = await self.send(cdp.dom.get_document(-1, True))
842
+
843
+ async def save_screenshot(
844
+ self,
845
+ filename: typing.Optional[PathLike] = "auto",
846
+ format: typing.Optional[str] = "jpeg",
847
+ scale: typing.Optional[typing.Union[int, float]] = 1,
848
+ ):
849
+ """
850
+ Saves a screenshot of this element (only)
851
+ This is not the same as :py:obj:`Tab.save_screenshot`, which saves a "regular" screenshot
852
+
853
+ When the element is hidden, or has no size, or is otherwise not capturable, a RuntimeError is raised
854
+
855
+ :param filename: uses this as the save path
856
+ :type filename: PathLike
857
+ :param format: jpeg or png (defaults to jpeg)
858
+ :type format: str
859
+ :param scale: the scale of the screenshot, eg: 1 = size as is, 2 = double, 0.5 is half
860
+ :return: the path/filename of saved screenshot
861
+ :rtype: str
862
+ """
863
+
864
+ import base64
865
+ import datetime
866
+ import urllib.parse
867
+
868
+ pos = await self.get_position()
869
+ if not pos:
870
+ raise RuntimeError(
871
+ "could not determine position of element. probably because it's not in view, or hidden"
872
+ )
873
+ viewport = pos.to_viewport(scale)
874
+ path = None
875
+ await self.tab.sleep()
876
+ if not filename or filename == "auto":
877
+ parsed = urllib.parse.urlparse(self.tab.target.url)
878
+ parts = parsed.path.split("/")
879
+ last_part = parts[-1]
880
+ last_part = last_part.rsplit("?", 1)[0]
881
+ dt_str = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
882
+ candidate = f"{parsed.hostname}__{last_part}_{dt_str}"
883
+ ext = ""
884
+ if format.lower() in ["jpg", "jpeg"]:
885
+ ext = ".jpg"
886
+ format = "jpeg"
887
+ elif format.lower() in ["png"]:
888
+ ext = ".png"
889
+ format = "png"
890
+ path = pathlib.Path(candidate + ext)
891
+ else:
892
+ path = pathlib.Path(filename)
893
+
894
+ path.parent.mkdir(parents=True, exist_ok=True)
895
+ data = await self._tab.send(
896
+ cdp.page.capture_screenshot(
897
+ format, clip=viewport, capture_beyond_viewport=True
898
+ )
899
+ )
900
+ if not data:
901
+ from .connection import ProtocolException
902
+
903
+ raise ProtocolException(
904
+ "could not take screenshot. most possible cause is the page has not finished loading yet."
905
+ )
906
+
907
+ data_bytes = base64.b64decode(data)
908
+ if not path:
909
+ raise RuntimeError("invalid filename or path: '%s'" % filename)
910
+ path.write_bytes(data_bytes)
911
+ return str(path)
912
+
913
+ async def flash(self, duration: typing.Union[float, int] = 0.5):
914
+ """
915
+ displays for a short time a red dot on the element (only if the element itself is visible)
916
+
917
+ :param coords: x,y
918
+ :type coords: x,y
919
+ :param duration: seconds (default 0.5)
920
+ :type duration:
921
+ :return:
922
+ :rtype:
923
+ """
924
+ from .connection import ProtocolException
925
+
926
+ if not self.remote_object:
927
+ try:
928
+ self._remote_object = await self.tab.send(
929
+ cdp.dom.resolve_node(backend_node_id=self.backend_node_id)
930
+ )
931
+ except ProtocolException:
932
+ return
933
+ try:
934
+ pos = await self.get_position()
935
+
936
+ except (Exception,):
937
+ logger.debug("flash() : could not determine position")
938
+ return
939
+ try:
940
+ await self._tab.flash_point(*pos.center)
941
+ except (Exception,):
942
+ pass
943
+ #
944
+ # style = (
945
+ # "position:absolute;z-index:99999999;padding:0;margin:0;"
946
+ # "left:{:.1f}px; top: {:.1f}px;"
947
+ # "opacity:1;"
948
+ # "width:16px;height:16px;border-radius:50%;background:red;"
949
+ # "animation:show-pointer-ani {:.2f}s ease 1;"
950
+ # ).format(
951
+ # pos.center[0] - 8, # -8 to account for drawn circle itself (w,h)
952
+ # pos.center[1] - 8,
953
+ # duration,
954
+ # )
955
+ # script = (
956
+ # """
957
+ # (targetElement) => {{
958
+ # var css = document.styleSheets[0];
959
+ # for( let css of [...document.styleSheets]) {{
960
+ # try {{
961
+ # css.insertRule(`
962
+ # @keyframes show-pointer-ani {{
963
+ # 0% {{ opacity: 1; transform: scale(2, 2);}}
964
+ # 25% {{ transform: scale(5,5) }}
965
+ # 50% {{ transform: scale(3, 3);}}
966
+ # 75%: {{ transform: scale(2,2) }}
967
+ # 100% {{ transform: scale(1, 1); opacity: 0;}}
968
+ # }}`,css.cssRules.length);
969
+ # break;
970
+ # }} catch (e) {{
971
+ # console.log(e)
972
+ # }}
973
+ # }};
974
+ # var _d = document.createElement('div');
975
+ # _d.style = `{0:s}`;
976
+ # _d.id = `{1:s}`;
977
+ # document.body.insertAdjacentElement('afterBegin', _d);
978
+ #
979
+ # setTimeout( () => document.getElementById('{1:s}').remove(), {2:d});
980
+ # }}
981
+ # """.format(
982
+ # style,
983
+ # secrets.token_hex(8),
984
+ # int(duration * 1000),
985
+ # )
986
+ # .replace(" ", "")
987
+ # .replace("\n", "")
988
+ # )
989
+ #
990
+ # arguments = [cdp.runtime.CallArgument(object_id=self._remote_object.object_id)]
991
+ # await self._tab.send(
992
+ # cdp.runtime.call_function_on(
993
+ # script,
994
+ # object_id=self._remote_object.object_id,
995
+ # arguments=arguments,
996
+ # await_promise=True,
997
+ # user_gesture=True,
998
+ # )
999
+ # )
1000
+
1001
+ async def highlight_overlay(self):
1002
+ """
1003
+ highlights the element devtools-style. To remove the highlight,
1004
+ call the method again.
1005
+ :return:
1006
+ :rtype:
1007
+ """
1008
+
1009
+ if getattr(self, "_is_highlighted", False):
1010
+ del self._is_highlighted
1011
+ await self.tab.send(cdp.overlay.hide_highlight())
1012
+ await self.tab.send(cdp.dom.disable())
1013
+ await self.tab.send(cdp.overlay.disable())
1014
+ return
1015
+ await self.tab.send(cdp.dom.enable())
1016
+ await self.tab.send(cdp.overlay.enable())
1017
+ conf = cdp.overlay.HighlightConfig(
1018
+ show_info=True, show_extension_lines=True, show_styles=True
1019
+ )
1020
+ await self.tab.send(
1021
+ cdp.overlay.highlight_node(
1022
+ highlight_config=conf, backend_node_id=self.backend_node_id
1023
+ )
1024
+ )
1025
+ setattr(self, "_is_highlighted", 1)
1026
+
1027
+ async def record_video(
1028
+ self,
1029
+ filename: typing.Optional[str] = None,
1030
+ folder: typing.Optional[str] = None,
1031
+ duration: typing.Optional[typing.Union[int, float]] = None,
1032
+ ):
1033
+ """
1034
+ experimental option.
1035
+
1036
+ :param filename: the desired filename
1037
+ :param folder: the download folder path
1038
+ :param duration: record for this many seconds and then download
1039
+
1040
+ on html5 video nodes, you can call this method to start recording of the video.
1041
+
1042
+ when any of the follow happens:
1043
+
1044
+ - video ends
1045
+ - calling videoelement('pause')
1046
+ - video stops
1047
+
1048
+ the video recorded will be downloaded.
1049
+
1050
+ """
1051
+ if self.node_name != "VIDEO":
1052
+ raise RuntimeError(
1053
+ "record_video can only be called on html5 video elements"
1054
+ )
1055
+ if not folder:
1056
+ directory_path = pathlib.Path.cwd() / "downloads"
1057
+ else:
1058
+ directory_path = pathlib.Path(folder)
1059
+
1060
+ directory_path.mkdir(exist_ok=True)
1061
+ await self._tab.send(
1062
+ cdp.browser.set_download_behavior(
1063
+ "allow", download_path=str(directory_path)
1064
+ )
1065
+ )
1066
+ await self("pause")
1067
+ await self.apply(
1068
+ """
1069
+ function extractVid(vid) {{
1070
+
1071
+ var duration = {duration:.1f};
1072
+ var stream = vid.captureStream();
1073
+ var mr = new MediaRecorder(stream, {{audio:true, video:true}})
1074
+ mr.ondataavailable = function(e) {{
1075
+ vid['_recording'] = false
1076
+ var blob = e.data;
1077
+ f = new File([blob], {{name: {filename}, type:'octet/stream'}});
1078
+ var objectUrl = URL.createObjectURL(f);
1079
+ var link = document.createElement('a');
1080
+ link.setAttribute('href', objectUrl)
1081
+ link.setAttribute('download', {filename})
1082
+ link.style.display = 'none'
1083
+
1084
+ document.body.appendChild(link)
1085
+
1086
+ link.click()
1087
+
1088
+ document.body.removeChild(link)
1089
+ }}
1090
+
1091
+ mr.start()
1092
+ vid.addEventListener('ended' , (e) => mr.stop())
1093
+ vid.addEventListener('pause' , (e) => mr.stop())
1094
+ vid.addEventListener('abort', (e) => mr.stop())
1095
+
1096
+
1097
+ if ( duration ) {{
1098
+ setTimeout(() => {{ vid.pause(); vid.play() }}, duration);
1099
+ }}
1100
+ vid['_recording'] = true
1101
+ ;}}
1102
+
1103
+ """.format(
1104
+ filename=f'"{filename}"' if filename else 'document.title + ".mp4"',
1105
+ duration=int(duration * 1000) if duration else 0,
1106
+ )
1107
+ )
1108
+ await self("play")
1109
+ await self._tab
1110
+
1111
+ async def is_recording(self):
1112
+ return await self.apply('(vid) => vid["_recording"]')
1113
+
1114
+ def _make_attrs(self):
1115
+ sav = None
1116
+ if self.node.attributes:
1117
+ for i, a in enumerate(self.node.attributes):
1118
+ if i == 0 or i % 2 == 0:
1119
+ if a == "class":
1120
+ a = "class_"
1121
+ sav = a
1122
+ else:
1123
+ if sav:
1124
+ self.attrs[sav] = a
1125
+
1126
+ def __eq__(self, other: Element) -> bool:
1127
+ # if other.__dict__.values() == self.__dict__.values():
1128
+ if not other:
1129
+ return False
1130
+ if other.backend_node_id and self.backend_node_id:
1131
+ return other.backend_node_id == self.backend_node_id
1132
+
1133
+ return False
1134
+
1135
+ def __repr__(self):
1136
+ tag_name = self.node.node_name.lower()
1137
+ content = ""
1138
+
1139
+ # collect all text from this leaf
1140
+ if self.child_node_count:
1141
+ if self.child_node_count == 1:
1142
+ if self.children:
1143
+ content += str(self.children[0])
1144
+
1145
+ elif self.child_node_count > 1:
1146
+ if self.children:
1147
+ for child in self.children:
1148
+ content += str(child)
1149
+
1150
+ if self.node.node_type == 3: # we could be a text node ourselves
1151
+ content += self.node_value
1152
+
1153
+ # return text only, no tag names
1154
+ # this makes it look most natural, and compatible with other hml libs
1155
+
1156
+ return content
1157
+
1158
+ attrs = " ".join(
1159
+ [f'{k if k != "class_" else "class"}="{v}"' for k, v in self.attrs.items()]
1160
+ )
1161
+ s = f"<{tag_name} {attrs}>{content}</{tag_name}>"
1162
+ return s
1163
+
1164
+
1165
+ class Position(cdp.dom.Quad):
1166
+ """helper class for element positioning"""
1167
+
1168
+ def __init__(self, points):
1169
+ super().__init__(points)
1170
+ (
1171
+ self.left,
1172
+ self.top,
1173
+ self.right,
1174
+ self.top,
1175
+ self.right,
1176
+ self.bottom,
1177
+ self.left,
1178
+ self.bottom,
1179
+ ) = points
1180
+ self.abs_x: float = 0
1181
+ self.abs_y: float = 0
1182
+ self.x = self.left
1183
+ self.y = self.top
1184
+ self.height, self.width = (self.bottom - self.top, self.right - self.left)
1185
+ self.center = (
1186
+ self.left + (self.width / 2),
1187
+ self.top + (self.height / 2),
1188
+ )
1189
+
1190
+ def to_viewport(self, scale=1):
1191
+ return cdp.page.Viewport(
1192
+ x=self.x, y=self.y, width=self.width, height=self.height, scale=scale
1193
+ )
1194
+
1195
+ def __repr__(self):
1196
+ return f"<Position(x={self.left}, y={self.top}, width={self.width}, height={self.height})>"
1197
+
1198
+
1199
+ async def resolve_node(tab: Tab, node_id: cdp.dom.NodeId):
1200
+ remote_obj: cdp.runtime.RemoteObject = await tab.send(
1201
+ cdp.dom.resolve_node(node_id=node_id)
1202
+ )
1203
+ node_id: cdp.dom.NodeId = await tab.send(cdp.dom.request_node(remote_obj.object_id))
1204
+ node: cdp.dom.Node = await tab.send(cdp.dom.describe_node(node_id))
1205
+ return node