pydoll-python 1.2.0__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.
pydoll/element.py ADDED
@@ -0,0 +1,313 @@
1
+ import asyncio
2
+ import json
3
+
4
+ import aiofiles
5
+ from bs4 import BeautifulSoup
6
+
7
+ from pydoll import exceptions
8
+ from pydoll.commands.dom import DomCommands
9
+ from pydoll.commands.input import InputCommands
10
+ from pydoll.commands.page import PageCommands
11
+ from pydoll.commands.runtime import RuntimeCommands
12
+ from pydoll.connection.connection import ConnectionHandler
13
+ from pydoll.constants import Scripts
14
+ from pydoll.mixins.find_elements import FindElementsMixin
15
+ from pydoll.utils import decode_image_to_bytes
16
+
17
+
18
+ class WebElement(FindElementsMixin):
19
+ def __init__(
20
+ self,
21
+ object_id: str,
22
+ connection_handler: ConnectionHandler,
23
+ method: str = None,
24
+ selector: str = None,
25
+ attributes_list: list = [],
26
+ ):
27
+ """
28
+ Initializes the WebElement instance.
29
+
30
+ Args:
31
+ node (dict): The node description from the browser.
32
+ connection_handler (ConnectionHandler): The connection instance.
33
+ """
34
+ self._object_id = object_id
35
+ self._search_method = method
36
+ self._selector = selector
37
+ self._connection_handler = connection_handler
38
+ self._attributes = {}
39
+ self._def_attributes(attributes_list)
40
+
41
+ def __repr__(self):
42
+ attrs = ', '.join(f'{k}={v!r}' for k, v in self._attributes.items())
43
+ return (
44
+ f'{self.__class__.__name__}({attrs})(object_id={self._object_id})'
45
+ )
46
+
47
+ def _def_attributes(self, attributes_list: list):
48
+ for i in range(0, len(attributes_list), 2):
49
+ key = attributes_list[i]
50
+ key = key if key != 'class' else 'class_name'
51
+ value = attributes_list[i + 1]
52
+ self._attributes[key] = value
53
+
54
+ @property
55
+ def value(self) -> str:
56
+ """
57
+ Retrieves the value of the element.
58
+
59
+ Returns:
60
+ str: The value of the element.
61
+ """
62
+ return self._attributes.get('value')
63
+
64
+ @property
65
+ def class_name(self) -> str:
66
+ """
67
+ Retrieves the class name of the
68
+ element.
69
+
70
+ Returns:
71
+ str: The class name of the
72
+ element.
73
+
74
+ """
75
+ return self._attributes.get('class_name')
76
+
77
+ @property
78
+ def id(self) -> str:
79
+ """
80
+ Retrieves the id of the element.
81
+
82
+ Returns:
83
+ str: The id of the element.
84
+ """
85
+ return self._attributes.get('id')
86
+
87
+ @property
88
+ def is_enabled(self) -> bool:
89
+ """
90
+ Retrieves the enabled status of the element.
91
+
92
+ Returns:
93
+ bool: The enabled status of the element.
94
+ """
95
+ return bool('disabled' not in self._attributes.keys())
96
+
97
+ @property
98
+ async def bounds(self) -> list:
99
+ """
100
+ Asynchronously retrieves the bounding box of the element.
101
+
102
+ Returns:
103
+ dict: The bounding box of the element.
104
+ """
105
+ command = DomCommands.box_model(object_id=self._object_id)
106
+ response = await self._execute_command(command)
107
+ return response['result']['model']['content']
108
+
109
+ @property
110
+ async def inner_html(self) -> str:
111
+ """
112
+ Retrieves the inner HTML of the element.
113
+
114
+ Returns:
115
+ str: The inner HTML of the element.
116
+ """
117
+ command = DomCommands.get_outer_html(self._object_id)
118
+ response = await self._execute_command(command)
119
+ return response['result']['outerHTML']
120
+
121
+ async def get_bounds_using_js(self) -> list:
122
+ """
123
+ Retrieves the bounding box of the element using JavaScript.
124
+
125
+ Returns:
126
+ list: The bounding box of the element.
127
+ """
128
+ response = await self._execute_script(
129
+ Scripts.BOUNDS, return_by_value=True
130
+ )
131
+ return json.loads(response['result']['result']['value'])
132
+
133
+ async def _execute_script(
134
+ self, script: str, return_by_value: bool = False
135
+ ):
136
+ """
137
+ Executes a JavaScript script on the element.
138
+
139
+ Args:
140
+ script (str): The JavaScript script to execute.
141
+ """
142
+ return await self._execute_command(
143
+ RuntimeCommands.call_function_on(
144
+ self._object_id, script, return_by_value
145
+ )
146
+ )
147
+
148
+ async def _is_element_visible(self):
149
+ """
150
+ Verifies if the element is visible using JavaScript.
151
+ It uses the getBoundingClientRect method to check if the element is
152
+ within the viewport and the width and height are greater than 0.
153
+
154
+ Returns:
155
+ bool: True if the element is visible, False otherwise.
156
+ """
157
+ result = await self._execute_script(
158
+ Scripts.ELEMENT_VISIBLE, return_by_value=True
159
+ )
160
+ return result['result']['result']['value']
161
+
162
+ async def _is_element_on_top(self):
163
+ """
164
+ Verifies if the element is on top of the page using JavaScript.
165
+ It uses the elementFromPoint method to check if the element is the
166
+ topmost element at the center of its bounding box.
167
+
168
+ Returns:
169
+ bool: True if the element is on top of the page, False otherwise.
170
+ """
171
+ result = await self._execute_script(
172
+ Scripts.ELEMENT_ON_TOP, return_by_value=True
173
+ )
174
+ return result['result']['result']['value']
175
+
176
+ async def get_screenshot(self, path: str):
177
+ """
178
+ Takes a screenshot of the element.
179
+
180
+ Args:
181
+ path (str): The path where the screenshot will be saved.
182
+ """
183
+ bounds = await self.get_bounds_using_js()
184
+ clip = {
185
+ 'x': bounds['x'],
186
+ 'y': bounds['y'],
187
+ 'width': bounds['width'],
188
+ 'height': bounds['height'],
189
+ 'scale': 1,
190
+ }
191
+ screenshot = await self._connection_handler.execute_command(
192
+ PageCommands.screenshot(format='jpeg', clip=clip)
193
+ )
194
+ async with aiofiles.open(path, 'wb') as file:
195
+ image_bytes = decode_image_to_bytes(screenshot['result']['data'])
196
+ await file.write(image_bytes)
197
+
198
+ async def get_element_text(self) -> str:
199
+ """
200
+ Retrieves the text of the element.
201
+
202
+ Returns:
203
+ str: The text of the element.
204
+ """
205
+ outer_html = await self.inner_html
206
+ soup = BeautifulSoup(outer_html, 'html.parser')
207
+ text_inside = soup.get_text(strip=True)
208
+ return text_inside
209
+
210
+ def get_attribute(self, name: str) -> str:
211
+ """
212
+ Retrieves the attribute value of the element.
213
+
214
+ Args:
215
+ name (str): The name of the attribute.
216
+
217
+ Returns:
218
+ str: The value of the attribute.
219
+ """
220
+ return self._attributes.get(name)
221
+
222
+ async def scroll_into_view(self):
223
+ """
224
+ Scrolls the element into view.
225
+ """
226
+ command = DomCommands.scroll_into_view(object_id=self._object_id)
227
+ await self._execute_command(command)
228
+
229
+ async def click_using_js(self):
230
+ if self._is_option_tag():
231
+ return await self.click_option_tag()
232
+
233
+ await self.scroll_into_view()
234
+
235
+ if not await self._is_element_visible():
236
+ raise exceptions.ElementNotVisible(
237
+ 'Element is not visible on the page.'
238
+ )
239
+
240
+ result = await self._execute_script(
241
+ Scripts.CLICK, return_by_value=True
242
+ )
243
+ clicked = result['result']['result']['value']
244
+ if not clicked:
245
+ raise exceptions.ElementNotInteractable(
246
+ 'Element is not interactable.'
247
+ )
248
+
249
+ async def click(self, x_offset: int = 0, y_offset: int = 0):
250
+ if self._is_option_tag():
251
+ return await self.click_option_tag()
252
+
253
+ if not await self._is_element_visible():
254
+ raise exceptions.ElementNotVisible(
255
+ 'Element is not visible on the page.'
256
+ )
257
+
258
+ await self.scroll_into_view()
259
+
260
+ try:
261
+ element_bounds = await self.bounds
262
+ position_to_click = self._calculate_center(element_bounds)
263
+ position_to_click = (
264
+ position_to_click[0] + x_offset,
265
+ position_to_click[1] + y_offset,
266
+ )
267
+ except KeyError:
268
+ element_bounds = await self.get_bounds_using_js()
269
+ position_to_click = (
270
+ element_bounds['x'] + element_bounds['width'] / 2,
271
+ element_bounds['y'] + element_bounds['height'] / 2,
272
+ )
273
+
274
+ press_command = InputCommands.mouse_press(*position_to_click)
275
+ release_command = InputCommands.mouse_release(*position_to_click)
276
+ await self._connection_handler.execute_command(press_command)
277
+ await asyncio.sleep(0.1)
278
+ await self._connection_handler.execute_command(release_command)
279
+
280
+ async def click_option_tag(self):
281
+ script = Scripts.CLICK_OPTION_TAG.replace('{self.value}', self.value)
282
+ await self._execute_command(RuntimeCommands.evaluate_script(script))
283
+
284
+ async def send_keys(self, text: str):
285
+ """
286
+ Sends a sequence of keys to the element.
287
+
288
+ Args:
289
+ text (str): The text to send to the element.
290
+ """
291
+ await self._execute_command(InputCommands.insert_text(text))
292
+
293
+ async def type_keys(self, text: str):
294
+ """
295
+ Types in a realistic manner by sending keys one by one.
296
+
297
+ Args:
298
+ text (str): The text to send to the element.
299
+ """
300
+ for char in text:
301
+ await self._execute_command(InputCommands.key_press(char))
302
+ await asyncio.sleep(0.1)
303
+
304
+ def _is_option_tag(self):
305
+ return self._attributes['tag_name'].lower() == 'option'
306
+
307
+ @staticmethod
308
+ def _calculate_center(bounds: list) -> tuple:
309
+ x_values = [bounds[i] for i in range(0, len(bounds), 2)]
310
+ y_values = [bounds[i] for i in range(1, len(bounds), 2)]
311
+ x_center = sum(x_values) / len(x_values)
312
+ y_center = sum(y_values) / len(y_values)
313
+ return x_center, y_center
@@ -0,0 +1,13 @@
1
+ from pydoll.events.browser import BrowserEvents
2
+ from pydoll.events.dom import DomEvents
3
+ from pydoll.events.fetch import FetchEvents
4
+ from pydoll.events.network import NetworkEvents
5
+ from pydoll.events.page import PageEvents
6
+
7
+ __all__ = [
8
+ 'BrowserEvents',
9
+ 'DomEvents',
10
+ 'FetchEvents',
11
+ 'NetworkEvents',
12
+ 'PageEvents',
13
+ ]
@@ -0,0 +1,26 @@
1
+ class BrowserEvents:
2
+ """
3
+ A class to define the browser events available through the
4
+ Chrome DevTools Protocol (CDP). These events allow for monitoring
5
+ specific actions and states within the browser, such as downloads.
6
+ """
7
+
8
+ DOWNLOAD_PROGRESS = 'Browser.downloadProgress'
9
+ """
10
+ Event triggered when the download progress updates.
11
+
12
+ This event provides details about the ongoing download,
13
+ including the amount downloaded and the total size.
14
+ It is part of the CDP's capabilities for monitoring
15
+ download activities in the browser.
16
+ """
17
+
18
+ DOWNLOAD_WILL_BEGIN = 'Browser.downloadWillBegin'
19
+ """
20
+ Event triggered when a download is about to start.
21
+
22
+ This event notifies listeners before the download begins,
23
+ providing an opportunity to handle or react to download events.
24
+ This is part of the CDP's support for download management
25
+ in the browser.
26
+ """
pydoll/events/dom.py ADDED
@@ -0,0 +1,108 @@
1
+ class DomEvents:
2
+ """
3
+ A class to define the DOM events available through the
4
+ Chrome DevTools Protocol (CDP). These events allow for monitoring
5
+ changes and updates within the Document Object Model (DOM)
6
+ of a web page, enabling developers to react to specific
7
+ modifications and interactions with the DOM elements.
8
+ """
9
+
10
+ ATTRIBUTE_MODIFIED = 'DOM.attributeModified'
11
+ """
12
+ Event triggered when an attribute of a DOM node is modified.
13
+
14
+ This event provides information about the node affected and the
15
+ attribute that was changed. It is part of the CDP's capabilities
16
+ for tracking DOM changes and allows developers to respond
17
+ to attribute modifications in real time.
18
+ """
19
+
20
+ ATTRIBUTE_REMOVED = 'DOM.attributeRemoved'
21
+ """
22
+ Event triggered when an attribute of a DOM node is removed.
23
+
24
+ This event indicates that an attribute has been deleted from a
25
+ node, allowing developers to manage state or perform cleanup
26
+ based on the changes. It is supported by the CDP to ensure
27
+ developers can monitor DOM manipulations effectively.
28
+ """
29
+
30
+ CHARACTER_DATA_MODIFIED = 'DOM.characterDataModified'
31
+ """
32
+ Event triggered when the character data of a DOM node is modified.
33
+
34
+ This event informs listeners about changes in the text content
35
+ of a node, which is essential for applications that need to
36
+ reflect real-time updates in the UI based on data changes.
37
+ """
38
+
39
+ CHILD_NODE_COUNT_UPDATED = 'DOM.childNodeCountUpdated'
40
+ """
41
+ Event triggered when the number of child nodes of a DOM node is updated.
42
+
43
+ This event alerts developers when the number of children changes,
44
+ allowing them to react to structural changes in the DOM tree.
45
+ """
46
+
47
+ CHILD_NODE_INSERTED = 'DOM.childNodeInserted'
48
+ """
49
+ Event triggered when a new child node is inserted into a DOM node.
50
+
51
+ This event notifies listeners of new additions to the DOM,
52
+ enabling actions such as updating UI components or handling
53
+ related data.
54
+ """
55
+
56
+ CHILD_NODE_REMOVED = 'DOM.childNodeRemoved'
57
+ """
58
+ Event triggered when a child node is removed from a DOM node.
59
+
60
+ This event indicates that a child has been deleted, allowing
61
+ developers to manage their state or trigger updates based on
62
+ the removal of elements in the DOM.
63
+ """
64
+
65
+ DOCUMENT_UPDATED = 'DOM.documentUpdated'
66
+ """
67
+ Event triggered when the DOM document is updated.
68
+
69
+ This event signifies that changes have occurred at the document
70
+ level, prompting developers to refresh or update their views
71
+ accordingly.
72
+ """
73
+
74
+ SCROLLABLE_FLAG_UPDATED = 'DOM.scrollableFlagUpdated'
75
+ """
76
+ Event triggered when the scrollable flag of a DOM node is updated.
77
+
78
+ This event is useful for determining which elements in the DOM
79
+ can be scrolled, allowing for enhanced user interactions and
80
+ responsive designs.
81
+ """
82
+
83
+ SHADOW_ROOT_POPPED = 'DOM.shadowRootPopped'
84
+ """
85
+ Event triggered when a shadow root is popped from the stack.
86
+
87
+ This event indicates that a shadow DOM context has been removed,
88
+ which is relevant for applications utilizing shadow DOM features
89
+ for encapsulated styling and markup.
90
+ """
91
+
92
+ SHADOW_ROOT_PUSHED = 'DOM.shadowRootPushed'
93
+ """
94
+ Event triggered when a shadow root is pushed onto the stack.
95
+
96
+ This event signifies that a new shadow DOM context has been
97
+ created, allowing developers to manage and respond to changes
98
+ in encapsulated DOM structures.
99
+ """
100
+
101
+ TOP_LAYER_ELEMENTS_UPDATED = 'DOM.topLayerElementsUpdated'
102
+ """
103
+ Event triggered when the top layer elements in the DOM are updated.
104
+
105
+ This event allows for monitoring changes in the most visible
106
+ elements in the DOM, which is essential for managing UI states
107
+ and rendering updates.
108
+ """
pydoll/events/fetch.py ADDED
@@ -0,0 +1,29 @@
1
+ class FetchEvents:
2
+ """
3
+ A class to define the Fetch events available through the
4
+ Chrome DevTools Protocol (CDP). These events are related to
5
+ the management of network requests, allowing developers to
6
+ intercept, modify, and monitor HTTP requests and responses
7
+ made by the browser.
8
+ """
9
+
10
+ AUTH_REQUIRED = 'Fetch.authRequired'
11
+ """
12
+ Event triggered when authentication is required for a network
13
+ request.
14
+
15
+ This event allows developers to respond to authentication
16
+ challenges, enabling them to provide credentials or take
17
+ appropriate actions when the requested resource requires
18
+ authentication.
19
+ """
20
+
21
+ REQUEST_PAUSED = 'Fetch.requestPaused'
22
+ """
23
+ Event triggered when a network request is paused.
24
+
25
+ This event is particularly useful for developers who want to
26
+ analyze or modify requests before they are sent. When a request
27
+ is paused, it gives the opportunity to inspect the request data
28
+ or alter headers before resuming it.
29
+ """
@@ -0,0 +1,160 @@
1
+ class NetworkEvents:
2
+ """
3
+ A class that defines constants for various network-related events.
4
+
5
+ These constants can be used to identify and handle network interactions in
6
+ applications, particularly in event-driven architectures or APIs that
7
+ monitor network activity.
8
+ """
9
+
10
+ DATA_RECEIVED = 'Network.dataReceived'
11
+ """
12
+ Event triggered when data is received over the network.
13
+
14
+ This can include responses from HTTP requests, incoming WebSocket messages,
15
+ or data from other network interactions. Useful for tracking incoming data
16
+ flow.
17
+ """
18
+
19
+ EVENT_SOURCE_MESSAGE_RECEIVED = 'Network.eventSourceMessageReceived'
20
+ """
21
+ Event fired when a message is received from an EventSource.
22
+
23
+ Typically used for server-sent events (SSE), this event indicates that a
24
+ new message has been sent from the server to the client, enabling real-time
25
+ updates.
26
+ """
27
+
28
+ LOADING_FAILED = 'Network.loadingFailed'
29
+ """
30
+ Event that indicates a failure in loading a network resource.
31
+
32
+ This can occur due to various reasons, such as network errors, resource
33
+ not found, or permission issues. This event is critical for error handling
34
+ and debugging.
35
+ """
36
+
37
+ LOADING_FINISHED = 'Network.loadingFinished'
38
+ """
39
+ Event fired when a network loading operation is completed.
40
+
41
+ This event is triggered regardless of whether the loading was successful
42
+ or failed, making it useful for cleaning up or updating the user interface
43
+ after loading operations.
44
+ """
45
+
46
+ REQUEST_SERVED_FROM_CACHE = 'Network.requestServedFromCache'
47
+ """
48
+ Event indicating that a network request was fulfilled from the cache.
49
+
50
+ This helps identify when data is being retrieved from cache instead of
51
+ making a new network request, which can improve performance and reduce
52
+ latency.
53
+ """
54
+
55
+ REQUEST_WILL_BE_SENT = 'Network.requestWillBeSent'
56
+ """
57
+ Event triggered just before a network request is sent.
58
+
59
+ This is useful for logging, modifying request headers, or performing
60
+ actions before the actual request is made. It allows developers to
61
+ intercept and examine requests.
62
+ """
63
+
64
+ RESPONSE_RECEIVED = 'Network.responseReceived'
65
+ """
66
+ Event that indicates a response has been received from a network request.
67
+
68
+ This event contains details about the response, such as status codes,
69
+ headers, and the body of the response. It's crucial for processing the
70
+ results of network requests.
71
+ """
72
+
73
+ WEB_SOCKET_CLOSED = 'Network.webSocketClosed'
74
+ """
75
+ Event that occurs when a WebSocket connection has been closed.
76
+
77
+ This can happen due to normal closure, errors, or network interruptions.
78
+ Handling this event is important for managing WebSocket connections and
79
+ reconnection logic.
80
+ """
81
+
82
+ WEB_SOCKET_CREATED = 'Network.webSocketCreated'
83
+ """
84
+ Event fired when a new WebSocket connection is established.
85
+
86
+ This indicates that a WebSocket connection is active and ready for
87
+ communication, allowing developers to set up message handlers or perform
88
+ other initialization tasks.
89
+ """
90
+
91
+ WEB_SOCKET_FRAME_ERROR = 'Network.webSocketFrameError'
92
+ """
93
+ Event indicating that there was an error with a frame in a WebSocket
94
+ communication.
95
+
96
+ This can be used to handle specific frame-related errors and improve the
97
+ robustness of WebSocket implementations by allowing for error logging and
98
+ corrective actions.
99
+ """
100
+
101
+ WEB_SOCKET_FRAME_RECEIVED = 'Network.webSocketFrameReceived'
102
+ """
103
+ Event fired when a frame is received through a WebSocket.
104
+
105
+ This is essential for processing incoming messages and performing actions
106
+ based on the content of those messages.
107
+ """
108
+
109
+ WEB_SOCKET_FRAME_SENT = 'Network.webSocketFrameSent'
110
+ """
111
+ Event representing a frame that has been sent through a WebSocket.
112
+
113
+ This event can be used for logging sent messages, monitoring communication,
114
+ or performing actions after a message has been sent.
115
+ """
116
+
117
+ WEB_TRANSPORT_CLOSED = 'Network.webTransportClosed'
118
+ """
119
+ Event indicating that a web transport connection has been closed.
120
+
121
+ Web transport connections are often used for low-latency communication.
122
+ Handling this event is vital for ensuring that resources are properly
123
+ released and that the application can react to disconnections.
124
+ """
125
+
126
+ WEB_TRANSPORT_CONNECTION_ESTABLISHED = (
127
+ 'Network.webTransportConnectionEstablished'
128
+ )
129
+ """
130
+ Event fired when a web transport connection is successfully established.
131
+
132
+ This signifies that the connection is ready for use, allowing for
133
+ immediate data transmission and interaction.
134
+ """
135
+
136
+ WEB_TRANSPORT_CREATED = 'Network.webTransportCreated'
137
+ """
138
+ Event that signifies that a new web transport connection has been created.
139
+
140
+ This is useful for setting up communication channels and initializing
141
+ necessary resources for the newly created transport.
142
+ """
143
+
144
+ POLICY_UPDATED = 'Network.policyUpdated'
145
+ """
146
+ Event that indicates that the network policy has been updated.
147
+
148
+ This might relate to security, access controls, or other network-related
149
+ policies that affect how requests and responses are handled. It’s important
150
+ for maintaining compliance and security in applications.
151
+ """
152
+
153
+ REQUEST_INTERCEPTED = 'Network.requestIntercepted'
154
+ """
155
+ Event fired when a network request has been intercepted.
156
+
157
+ This is often used in service workers or other intermediary layers to
158
+ modify or block requests before they reach the network. Handling this event
159
+ is crucial for implementing custom request logic or caching strategies.
160
+ """