browser-toolkit 0.0.1a1__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.
- browser_toolkit/__init__.py +2 -0
- browser_toolkit/base_toolkit.py +584 -0
- browser_toolkit/camoufox.py +10 -0
- browser_toolkit/create_browser/__init__.py +0 -0
- browser_toolkit/create_browser/playwright.py +27 -0
- browser_toolkit/playwright.py +505 -0
- browser_toolkit/selenium.py +372 -0
- browser_toolkit/selenium_toolkit/__init__.py +1 -0
- browser_toolkit/selenium_toolkit/selenium_toolkit.py +408 -0
- browser_toolkit/selenium_toolkit/utils.py +10 -0
- browser_toolkit/types.py +49 -0
- browser_toolkit/utils.py +2 -0
- browser_toolkit-0.0.1a1.dist-info/METADATA +85 -0
- browser_toolkit-0.0.1a1.dist-info/RECORD +17 -0
- browser_toolkit-0.0.1a1.dist-info/WHEEL +5 -0
- browser_toolkit-0.0.1a1.dist-info/licenses/LICENSE +339 -0
- browser_toolkit-0.0.1a1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
from random import uniform
|
|
4
|
+
from typing import Union, Type
|
|
5
|
+
from http.cookies import SimpleCookie
|
|
6
|
+
|
|
7
|
+
from selenium.webdriver.chromium.webdriver import ChromiumDriver
|
|
8
|
+
from selenium.webdriver.common.by import By
|
|
9
|
+
from selenium.webdriver.support import expected_conditions as EC
|
|
10
|
+
from selenium.webdriver.support.wait import WebDriverWait
|
|
11
|
+
from selenium.common.exceptions import (
|
|
12
|
+
TimeoutException,
|
|
13
|
+
InvalidSessionIdException,
|
|
14
|
+
NoSuchElementException,
|
|
15
|
+
WebDriverException,
|
|
16
|
+
)
|
|
17
|
+
from selenium.webdriver.remote.webdriver import WebDriver, WebElement
|
|
18
|
+
|
|
19
|
+
from browser_toolkit.types import Request, RequestType, Redirect
|
|
20
|
+
from browser_toolkit.selenium_toolkit.utils import create_locator
|
|
21
|
+
import functools
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def auto_wait(func) -> Type["Response"]:
|
|
25
|
+
@functools.wraps(func)
|
|
26
|
+
def wrapper(*args, **kwargs):
|
|
27
|
+
obj_wait_time = args[0]._wait_time_range
|
|
28
|
+
wait = uniform(*obj_wait_time)
|
|
29
|
+
time.sleep(wait)
|
|
30
|
+
|
|
31
|
+
return func(*args, **kwargs)
|
|
32
|
+
|
|
33
|
+
return wrapper
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SeleniumToolKit:
|
|
37
|
+
_wait_time_range = (0, 0)
|
|
38
|
+
|
|
39
|
+
def __init__(self, driver):
|
|
40
|
+
self.__driver: Union[WebDriver, ChromiumDriver] = driver
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def driver(self) -> Union[WebDriver, ChromiumDriver]:
|
|
44
|
+
return self.__driver
|
|
45
|
+
|
|
46
|
+
def change_wait_time(self, range_time: tuple = (0, 0)):
|
|
47
|
+
first, last = range_time
|
|
48
|
+
|
|
49
|
+
if not (first >= 0 and last >= first):
|
|
50
|
+
raise ValueError(f"range_time must be a tuple with positive values")
|
|
51
|
+
|
|
52
|
+
self._wait_time_range = range_time
|
|
53
|
+
|
|
54
|
+
@auto_wait
|
|
55
|
+
def goto(self, url: str) -> None:
|
|
56
|
+
self.__driver.get(url=url)
|
|
57
|
+
|
|
58
|
+
def query_selector(self, query_selector: str, web_element: WebElement = None) -> Union[WebElement, None]:
|
|
59
|
+
"""
|
|
60
|
+
:param query_selector: css or xpath
|
|
61
|
+
:param web_element: If None is passed will perform the query selector in the whole page
|
|
62
|
+
"""
|
|
63
|
+
if not query_selector:
|
|
64
|
+
raise ValueError("You need send a query_selector")
|
|
65
|
+
|
|
66
|
+
target = web_element if web_element else self.__driver
|
|
67
|
+
|
|
68
|
+
if query_selector[0] == "/":
|
|
69
|
+
web_element = target.find_element(By.XPATH, query_selector)
|
|
70
|
+
else:
|
|
71
|
+
web_element = target.find_element(By.CSS_SELECTOR, query_selector)
|
|
72
|
+
|
|
73
|
+
return web_element
|
|
74
|
+
|
|
75
|
+
def query_selector_all(self, query_selector: str, web_element: WebElement = None) -> Union[list[WebElement], None]:
|
|
76
|
+
"""
|
|
77
|
+
:param query_selector: css or xpath
|
|
78
|
+
:param web_element: If None is passed will perform the query selector in the whole page
|
|
79
|
+
"""
|
|
80
|
+
if not query_selector:
|
|
81
|
+
raise ValueError("You need send a query_selector")
|
|
82
|
+
|
|
83
|
+
target = web_element if web_element else self.__driver
|
|
84
|
+
|
|
85
|
+
if query_selector[0] == "/":
|
|
86
|
+
web_elements = target.find_elements(By.XPATH, query_selector)
|
|
87
|
+
else:
|
|
88
|
+
web_elements = target.find_elements(By.CSS_SELECTOR, query_selector)
|
|
89
|
+
|
|
90
|
+
return web_elements
|
|
91
|
+
|
|
92
|
+
def find_element_by_text(self, text: str):
|
|
93
|
+
query_selector = f"//*[contains(text(), '{text}' )]"
|
|
94
|
+
web_element = self.query_selector(query_selector=query_selector)
|
|
95
|
+
return web_element
|
|
96
|
+
|
|
97
|
+
def find_elements_by_text(self, text: str):
|
|
98
|
+
query_selector = f"//*[contains(text(), '{text}' )]"
|
|
99
|
+
web_element = self.query_selector_all(query_selector=query_selector)
|
|
100
|
+
return web_element
|
|
101
|
+
|
|
102
|
+
def find_element_by_tag_and_text(self, tag: str, text: str):
|
|
103
|
+
query_selector = f"//{tag}[contains(text(), '{text}' )]"
|
|
104
|
+
web_elements = self.query_selector(query_selector=query_selector)
|
|
105
|
+
return web_elements
|
|
106
|
+
|
|
107
|
+
def find_elements_by_tag_and_text(self, tag: str, text: str):
|
|
108
|
+
query_selector = f"//{tag}[contains(text(), '{text}' )]"
|
|
109
|
+
web_elements = self.query_selector_all(query_selector=query_selector)
|
|
110
|
+
return web_elements
|
|
111
|
+
|
|
112
|
+
def get_text(self, query_selector: str) -> str:
|
|
113
|
+
try:
|
|
114
|
+
return self.query_selector(query_selector=query_selector).text
|
|
115
|
+
except NoSuchElementException as e:
|
|
116
|
+
raise e
|
|
117
|
+
|
|
118
|
+
def get_attribute(self, query_selector: str, attribute: str) -> str:
|
|
119
|
+
try:
|
|
120
|
+
return self.query_selector(query_selector=query_selector).get_attribute(attribute)
|
|
121
|
+
except NoSuchElementException as e:
|
|
122
|
+
raise e
|
|
123
|
+
|
|
124
|
+
@auto_wait
|
|
125
|
+
def click(self, query_selector: str) -> None:
|
|
126
|
+
self.query_selector(query_selector=query_selector).click()
|
|
127
|
+
|
|
128
|
+
@auto_wait
|
|
129
|
+
def fill(self, text: str, query_selector: str) -> None:
|
|
130
|
+
element = self.query_selector(query_selector=query_selector)
|
|
131
|
+
element.send_keys(text)
|
|
132
|
+
|
|
133
|
+
@auto_wait
|
|
134
|
+
def clear(self, query_selector: str) -> None:
|
|
135
|
+
self.query_selector(query_selector=query_selector).clear()
|
|
136
|
+
|
|
137
|
+
def fill_in_random_time(self, text: str, query_selector: str) -> None:
|
|
138
|
+
element = self.query_selector(query_selector=query_selector)
|
|
139
|
+
for letter in text:
|
|
140
|
+
time.sleep(uniform(0.3, 0.8))
|
|
141
|
+
element.send_keys(letter)
|
|
142
|
+
|
|
143
|
+
def clear_and_fill(self, text: str, query_selector: str, random_time=False) -> None:
|
|
144
|
+
self.clear(query_selector=query_selector)
|
|
145
|
+
if random_time:
|
|
146
|
+
self.fill_in_random_time(text=text, query_selector=query_selector)
|
|
147
|
+
else:
|
|
148
|
+
self.fill(text=text, query_selector=query_selector)
|
|
149
|
+
|
|
150
|
+
def element_is_present(self, wait_time: int, query_selector: str) -> bool:
|
|
151
|
+
try:
|
|
152
|
+
WebDriverWait(self.__driver, wait_time).until(
|
|
153
|
+
EC.presence_of_element_located(create_locator(query_selector))
|
|
154
|
+
)
|
|
155
|
+
return True
|
|
156
|
+
except TimeoutException:
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
def element_is_visible(self, wait_time: int, query_selector: str) -> bool:
|
|
160
|
+
try:
|
|
161
|
+
WebDriverWait(self.__driver, wait_time).until(
|
|
162
|
+
EC.visibility_of_element_located(create_locator(query_selector))
|
|
163
|
+
)
|
|
164
|
+
return True
|
|
165
|
+
except TimeoutException:
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
def element_is_invisible(self, wait_time: int, query_selector: str) -> bool:
|
|
169
|
+
try:
|
|
170
|
+
WebDriverWait(self.__driver, wait_time).until(
|
|
171
|
+
EC.invisibility_of_element_located(create_locator(query_selector))
|
|
172
|
+
)
|
|
173
|
+
return True
|
|
174
|
+
except TimeoutException:
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
def element_is_clickable(self, wait_time: int, query_selector: str) -> bool:
|
|
178
|
+
try:
|
|
179
|
+
WebDriverWait(self.__driver, wait_time).until(EC.element_to_be_clickable(create_locator(query_selector)))
|
|
180
|
+
return True
|
|
181
|
+
except TimeoutException:
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
def text_is_present(self, wait_time: int, query_selector: str, text: str) -> bool:
|
|
185
|
+
try:
|
|
186
|
+
WebDriverWait(self.__driver, wait_time).until(
|
|
187
|
+
EC.text_to_be_present_in_element(create_locator(query_selector), text_=text)
|
|
188
|
+
)
|
|
189
|
+
return True
|
|
190
|
+
except TimeoutException:
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
def alert_is_present(self, wait_time: int, message: str) -> bool:
|
|
194
|
+
try:
|
|
195
|
+
WebDriverWait(self.__driver, wait_time).until(EC.alert_is_present(), message=message)
|
|
196
|
+
return True
|
|
197
|
+
except TimeoutException:
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
def page_is_loading(self) -> bool:
|
|
201
|
+
if self.__driver.execute_script("return document.readyState") != "complete":
|
|
202
|
+
return True
|
|
203
|
+
else:
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
def block_urls(self, urls: list) -> None:
|
|
207
|
+
if not isinstance(self.__driver, ChromiumDriver):
|
|
208
|
+
TypeError("Your driver must be a ChromiumDriver type to use this method")
|
|
209
|
+
|
|
210
|
+
self.execute_cdp_cmd("Network.setBlockedURLs", {"urls": urls})
|
|
211
|
+
self.execute_cdp_cmd("Network.enable", {})
|
|
212
|
+
|
|
213
|
+
def driver_hard_refresh(self) -> None:
|
|
214
|
+
self.__driver.execute_script("location.reload(true)")
|
|
215
|
+
|
|
216
|
+
def webdriver_is_open(self) -> bool:
|
|
217
|
+
try:
|
|
218
|
+
self.__driver.execute_script("console.log('ola eu estou funcionando');")
|
|
219
|
+
return True
|
|
220
|
+
except InvalidSessionIdException:
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
def get_all_requests(self) -> list[dict]:
|
|
224
|
+
"""
|
|
225
|
+
!!! ALERT !!!
|
|
226
|
+
For this method works the code below is necessary in the driver's creation
|
|
227
|
+
|
|
228
|
+
# selenium < 4.0
|
|
229
|
+
capabilities = DesiredCapabilities.CHROME
|
|
230
|
+
capabilities["goog:loggingPrefs"] = {"performance": "ALL"}
|
|
231
|
+
driver = webdriver.Chrome(desired_capabilities=capabilities
|
|
232
|
+
|
|
233
|
+
# selenium > 4.0
|
|
234
|
+
capabilities = {"performance": "ALL"}
|
|
235
|
+
options.set_capability("goog:loggingPrefs", capabilities)
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
if not isinstance(self.__driver, ChromiumDriver):
|
|
239
|
+
TypeError("Your driver must be a ChromiumDriver type to use this method")
|
|
240
|
+
|
|
241
|
+
logs_raw = self.__driver.get_log("performance")
|
|
242
|
+
parsed_logs = [json.loads(lr["message"])["message"] for lr in logs_raw]
|
|
243
|
+
return parsed_logs
|
|
244
|
+
|
|
245
|
+
def get_requests(self, request_url: str) -> list[Request] | None:
|
|
246
|
+
parsed_logs = self.get_all_requests()
|
|
247
|
+
methods = [
|
|
248
|
+
"Network.responseReceived",
|
|
249
|
+
"Network.requestWillBeSent",
|
|
250
|
+
"Network.requestWillBeSentExtraInfo",
|
|
251
|
+
# "Page.windowOpen" # I Only see in Redirect, maybe add in the future, does not have request_id
|
|
252
|
+
]
|
|
253
|
+
received_response_list = [response for response in parsed_logs if response["method"] in methods]
|
|
254
|
+
|
|
255
|
+
resp_url = None
|
|
256
|
+
matched_requests_id = set()
|
|
257
|
+
for response in received_response_list:
|
|
258
|
+
urls_to_match = []
|
|
259
|
+
params = response["params"]
|
|
260
|
+
target_request_id = params.get("requestId")
|
|
261
|
+
if params.get("request"):
|
|
262
|
+
urls_to_match.append(params["request"]["url"])
|
|
263
|
+
if params.get("response"):
|
|
264
|
+
urls_to_match.append(params["response"]["url"])
|
|
265
|
+
if params.get("redirectResponse"):
|
|
266
|
+
urls_to_match.append(params["redirectResponse"]["url"])
|
|
267
|
+
|
|
268
|
+
for url in urls_to_match:
|
|
269
|
+
if request_url in url:
|
|
270
|
+
matched_requests_id.add(target_request_id)
|
|
271
|
+
|
|
272
|
+
if not matched_requests_id:
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
matched_requests = []
|
|
276
|
+
for target_request_id in matched_requests_id:
|
|
277
|
+
cookies = dict()
|
|
278
|
+
headers = dict()
|
|
279
|
+
url: str = None
|
|
280
|
+
redirect = None
|
|
281
|
+
request_type: RequestType = None
|
|
282
|
+
for response in received_response_list:
|
|
283
|
+
params = response["params"]
|
|
284
|
+
request_id = params.get("requestId")
|
|
285
|
+
method = response.get("method")
|
|
286
|
+
|
|
287
|
+
if target_request_id == request_id:
|
|
288
|
+
if method == "Network.requestWillBeSentExtraInfo":
|
|
289
|
+
headers = params.get("headers")
|
|
290
|
+
|
|
291
|
+
cookies_string = headers.get("cookie")
|
|
292
|
+
if not cookies_string:
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
cookie_parser = SimpleCookie()
|
|
296
|
+
cookie_parser.load(cookies_string)
|
|
297
|
+
cookies = dict(cookie_parser)
|
|
298
|
+
|
|
299
|
+
if method == "Network.requestWillBeSent":
|
|
300
|
+
url = params["request"]["url"]
|
|
301
|
+
request_type = RequestType(params["type"])
|
|
302
|
+
|
|
303
|
+
if params.get("redirectResponse"):
|
|
304
|
+
redirect_url = params["redirectResponse"]["url"]
|
|
305
|
+
if request_url in redirect_url:
|
|
306
|
+
redirect = Redirect(url=redirect_url)
|
|
307
|
+
|
|
308
|
+
request_data = Request(
|
|
309
|
+
url=url,
|
|
310
|
+
request_id=target_request_id,
|
|
311
|
+
cookies=cookies,
|
|
312
|
+
headers=headers,
|
|
313
|
+
redirect=redirect,
|
|
314
|
+
type=request_type,
|
|
315
|
+
)
|
|
316
|
+
matched_requests.append(request_data)
|
|
317
|
+
|
|
318
|
+
return matched_requests
|
|
319
|
+
|
|
320
|
+
def response_data_from_request(self, request_url: str, request_id: str = None) -> str | None:
|
|
321
|
+
if request_id:
|
|
322
|
+
response_body = self.execute_cdp_cmd("Network.getResponseBody", {"requestId": request_id})
|
|
323
|
+
return response_body
|
|
324
|
+
|
|
325
|
+
received_requests = self.get_requests(request_url=request_url)
|
|
326
|
+
if not received_requests:
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
if len(received_requests) > 1:
|
|
330
|
+
raise ValueError("more than one request matched")
|
|
331
|
+
|
|
332
|
+
return self.get_response_body_from_request_id(request_id=received_requests[0].request_id)
|
|
333
|
+
|
|
334
|
+
def get_response_body_from_request_id(self, request_id: str = None) -> str | None:
|
|
335
|
+
try:
|
|
336
|
+
response_body = self.execute_cdp_cmd(cmd="Network.getResponseBody", cmd_args={"requestId": request_id})
|
|
337
|
+
except WebDriverException:
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
return response_body
|
|
341
|
+
|
|
342
|
+
def execute_cdp_cmd(self, cmd: str, cmd_args: dict) -> str:
|
|
343
|
+
"""
|
|
344
|
+
Useful for when executing CDP command in a remote driver
|
|
345
|
+
"""
|
|
346
|
+
resource = "/session/%s/chromium/send_command_and_get_result" % self.__driver.session_id
|
|
347
|
+
url = self.__driver.command_executor._url + resource
|
|
348
|
+
body = json.dumps({"cmd": cmd, "params": cmd_args})
|
|
349
|
+
response = self.__driver.command_executor._request("POST", url, body)
|
|
350
|
+
return response.get("value")
|
|
351
|
+
|
|
352
|
+
def scroll_window(self, query_selector: str = None, web_element: WebElement = None) -> None:
|
|
353
|
+
"""
|
|
354
|
+
Scrolls window to element position
|
|
355
|
+
"""
|
|
356
|
+
assert query_selector or web_element, "You need to provide query_selector or web_element"
|
|
357
|
+
|
|
358
|
+
if web_element is None:
|
|
359
|
+
web_element = self.query_selector(query_selector=query_selector)
|
|
360
|
+
|
|
361
|
+
js_code = "arguments[0].scrollIntoView();"
|
|
362
|
+
self.__driver.execute_script(js_code, web_element)
|
|
363
|
+
|
|
364
|
+
def get_all_local_storage(
|
|
365
|
+
self,
|
|
366
|
+
) -> dict:
|
|
367
|
+
return self.__driver.execute_script(f"return window.localStorage")
|
|
368
|
+
|
|
369
|
+
def quit(self):
|
|
370
|
+
if self.webdriver_is_open():
|
|
371
|
+
self.__driver.quit()
|
|
372
|
+
return
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .selenium_toolkit import SeleniumToolKit
|