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.
@@ -0,0 +1,2 @@
1
+ from .playwright import PlaywrightTollKit
2
+ from .selenium_toolkit.selenium_toolkit import SeleniumToolKit
@@ -0,0 +1,584 @@
1
+ import asyncio
2
+ import functools
3
+ import inspect
4
+ from abc import ABC, abstractmethod
5
+ from datetime import datetime
6
+ from random import uniform
7
+ from typing import Self
8
+
9
+ from browser_toolkit.types import Cookie
10
+
11
+
12
+ class BaseWebElement(ABC):
13
+ """Base class for web elements"""
14
+
15
+ @abstractmethod
16
+ async def click(self, selector: str, delay: int = 0) -> None:
17
+ """
18
+ Clicks the element
19
+
20
+ :param selector: string - CSS selector or XPath
21
+ :param delay: int - time to keep the mouse button pressed in seconds (default: 0)
22
+ :return:
23
+ """
24
+ pass
25
+
26
+ @abstractmethod
27
+ async def click_js(self) -> None:
28
+ """
29
+ Clicks the element using JavaScript
30
+
31
+ :return:
32
+ """
33
+ pass
34
+
35
+ @abstractmethod
36
+ async def type(self, text: str, interval: float | int, clear_before: bool) -> None:
37
+ """
38
+ Fills the element with the text
39
+
40
+ :param text: string - text to fill
41
+ :param interval: float or int - time to wait between each character in seconds
42
+ :param clear_before: bool - whether to clear the field before filling
43
+ :return:
44
+ """
45
+ pass
46
+
47
+ @abstractmethod
48
+ async def clear(self) -> None:
49
+ """
50
+ Clears the element
51
+
52
+ :return:
53
+ """
54
+ pass
55
+
56
+ @abstractmethod
57
+ async def selector(self, selector: str) -> Self | None:
58
+ """
59
+ Queries the web_element and returns the first element matching the selector.
60
+ :param selector:
61
+ :return:
62
+ """
63
+ pass
64
+
65
+ @abstractmethod
66
+ async def selector_all(self, selector: str) -> list[Self]:
67
+ """
68
+ Queries the web_element and returns all elements matching the selector.
69
+ :param selector:
70
+ :return:
71
+ """
72
+ pass
73
+
74
+
75
+ def now_str() -> str:
76
+ return datetime.now().strftime("%Y-%m-%dT%H-%M-%S")
77
+
78
+
79
+ def main_decorator(func):
80
+ @functools.wraps(func)
81
+ async def wrapper(self: "BaseBrowserToolkit", *args, **kwargs):
82
+ has_screenshot = isinstance(self._screenshot_directory, str)
83
+ actions_names = [
84
+ # "goto",
85
+ "click",
86
+ "click_js",
87
+ "type",
88
+ "clear",
89
+ "scroll_to_element",
90
+ "scroll_to_top",
91
+ "scroll_to_bottom",
92
+ "reload",
93
+ "hard_reload",
94
+ ]
95
+ func_name = func.__name__
96
+
97
+ # Ignore save_screenshot
98
+ if func_name == "save_screenshot":
99
+ return await func(self, *args, **kwargs)
100
+
101
+ # Waits a random time before executing the function
102
+ sleep_time = uniform(*self._wait_time_range)
103
+ if func_name in actions_names or func_name == "goto":
104
+ await asyncio.sleep(sleep_time)
105
+
106
+ # Takes screenshot before executing the function
107
+ if func_name in actions_names and has_screenshot:
108
+ file_path = f"{self._screenshot_directory}/{now_str()}_{func_name}.jpeg"
109
+ await self.save_screenshot(file_path=file_path)
110
+
111
+ # Executes the function and catches exceptions to take a screenshot if it fails
112
+ try:
113
+ return await func(self, *args, **kwargs)
114
+ except Exception as e:
115
+ if has_screenshot:
116
+ file_path = f"{self._screenshot_directory}/{now_str()}_exeption.jpeg"
117
+ await self.save_screenshot(file_path=file_path)
118
+ raise e
119
+
120
+ return wrapper
121
+
122
+
123
+ class AutoDecorate:
124
+ def __init_subclass__(cls, **kwargs):
125
+ super().__init_subclass__(**kwargs)
126
+ for attr_name, attr_value in cls.__dict__.items():
127
+ if inspect.iscoroutinefunction(attr_value):
128
+ setattr(cls, attr_name, main_decorator(attr_value))
129
+
130
+
131
+ class BaseBrowserToolkit(ABC, AutoDecorate):
132
+ _wait_time_range = (0, 0)
133
+ _screenshot_directory: str | None = None
134
+
135
+ # def __init__(self, browser):
136
+ # self.browser = browser
137
+
138
+ # --------------------------- START decorators ---------------------------
139
+ async def change_wait_time(self, range_time: tuple = (0, 0)):
140
+ first, last = range_time
141
+
142
+ if not (first >= 0 and last >= first):
143
+ raise ValueError(f"range_time must be a tuple with positive values")
144
+
145
+ self._wait_time_range = range_time
146
+
147
+ async def change_screenshot_directory(self, screenshot_directory: str):
148
+ self._screenshot_directory = screenshot_directory
149
+
150
+ # --------------------------- END decorators ---------------------------
151
+
152
+ # --------------------------- START session management ---------------------------
153
+ @abstractmethod
154
+ async def close(self) -> None:
155
+ """
156
+ Closes the browser tab
157
+ :return:
158
+ """
159
+
160
+ # --------------------------- END session management ---------------------------
161
+
162
+ # --------------------------- START selectors ---------------------------
163
+ @abstractmethod
164
+ async def selector(self, selector: str, web_element: BaseWebElement | None = None) -> BaseWebElement | None:
165
+ """
166
+ Queries the page and returns the first element matching the selector.
167
+ If a web_element is provided, it queries within that element instead of the whole page.
168
+ :param selector:
169
+ :param web_element:
170
+ :return:
171
+ """
172
+ pass
173
+
174
+ @abstractmethod
175
+ async def selector_all(self, selector: str, web_element: BaseWebElement | None = None) -> list[BaseWebElement]:
176
+ """
177
+ Queries the page and returns all elements matching the selector.
178
+ If a web_element is provided, it queries within that element instead of the whole page.
179
+ :param selector:
180
+ :param web_element:
181
+ :return:
182
+ """
183
+ pass
184
+
185
+ # --------------------------- END selectors ---------------------------
186
+
187
+ # --------------------------- START Actions ---------------------------
188
+ @abstractmethod
189
+ async def goto(self, url: str, timeout: int = 30) -> None:
190
+ """
191
+ Navigates to URL
192
+
193
+ :param url: url to navigate to
194
+ :param timeout: maximum time to wait for the page to load in seconds
195
+ :return:
196
+ """
197
+
198
+ pass
199
+
200
+ @abstractmethod
201
+ async def click(self, selector: str, delay: int = 0) -> None:
202
+ """
203
+ Clicks the element matching the selector
204
+
205
+ :param selector: string - CSS selector or XPath
206
+ :param delay: int - time to keep the mouse button pressed in seconds (default: 0)
207
+ :return:
208
+ """
209
+ pass
210
+
211
+ @abstractmethod
212
+ async def click_js(self, selector: str) -> None:
213
+ """
214
+ Clicks the element matching the selector using JavaScript
215
+
216
+ :param selector: string - CSS selector or XPath
217
+ :return:
218
+ """
219
+ pass
220
+
221
+ @abstractmethod
222
+ async def type(self, text: str, selector: str, interval: float | int, clear_before: bool) -> None:
223
+ """
224
+ Fills the element matching the selector with the text
225
+
226
+ :param text: string - text to fill
227
+ :param selector: string - CSS selector or XPath
228
+ :param interval: float or int - time to wait between each character
229
+ :param clear_before: bool - whether to clear the field before filling
230
+ :return:
231
+ """
232
+ pass
233
+
234
+ @abstractmethod
235
+ async def clear(self, selector: str) -> None:
236
+ """
237
+ Clears the element matching the selector
238
+
239
+ :param selector: string - CSS selector or XPath
240
+ :return:
241
+ """
242
+ pass
243
+
244
+ @abstractmethod
245
+ async def scroll_to_element(self, selector: str) -> None:
246
+ """
247
+ Scrolls to the element matching the selector
248
+
249
+ :param selector: string - CSS selector or XPath
250
+ :return:
251
+ """
252
+ pass
253
+
254
+ @abstractmethod
255
+ async def scroll_to_top(self) -> None:
256
+ """
257
+ Scrolls to the top of the page
258
+
259
+ :return:
260
+ """
261
+ pass
262
+
263
+ @abstractmethod
264
+ async def scroll_to_bottom(self) -> None:
265
+ """
266
+ Scrolls to the bottom of the page
267
+
268
+ :return:
269
+ """
270
+ pass
271
+
272
+ @abstractmethod
273
+ async def reload(self) -> None:
274
+ """
275
+ Reloads the page
276
+
277
+ :return:
278
+ """
279
+ pass
280
+
281
+ @abstractmethod
282
+ async def hard_reload(self) -> None:
283
+ """
284
+ Hard reloads the page (ignoring cache)
285
+
286
+ :return:
287
+ """
288
+ pass
289
+
290
+ # --------------------------- END Actions ---------------------------
291
+
292
+ # --------------------------- START page data ---------------------------
293
+ @property
294
+ @abstractmethod
295
+ async def current_url(self) -> str:
296
+ """
297
+ Gets the current URL
298
+
299
+ :return: str - current URL
300
+ """
301
+ pass
302
+
303
+ @property
304
+ @abstractmethod
305
+ async def title(self) -> str:
306
+ """
307
+ Gets the page title
308
+
309
+ :return: str - page title
310
+ """
311
+ pass
312
+
313
+ @property
314
+ @abstractmethod
315
+ async def page_source(self) -> str:
316
+ """
317
+ Gets the page source
318
+
319
+ :return: str - page source
320
+ """
321
+ pass
322
+
323
+ @abstractmethod
324
+ async def get_text(self, selector: str) -> str:
325
+ """
326
+ Gets the text from the element matching the selector
327
+
328
+ :param selector: string - CSS selector or XPath
329
+ :return: str - text of the element
330
+ """
331
+
332
+ @abstractmethod
333
+ async def get_attribute(self, selector: str, attribute: str) -> str:
334
+ """
335
+ Gets the attribute from the element matching the selector
336
+
337
+ :param selector: string - CSS selector or XPath
338
+ :param attribute: string - attribute name
339
+ :return: str - attribute of the element
340
+ """
341
+ pass
342
+
343
+ @abstractmethod
344
+ async def save_screenshot(self, file_path: str) -> None:
345
+ """
346
+ Takes a screenshot of the current page and saves it to the exception directory if it is set
347
+ :param file_path:
348
+ :return:
349
+ """
350
+ pass
351
+
352
+ # --------------------------- END page data ---------------------------
353
+
354
+ # --------------------------- START network ---------------------------
355
+ @abstractmethod
356
+ async def get_network_requests(self) -> list[dict]:
357
+ """
358
+ Get all network Requests
359
+ :return:
360
+ """
361
+ pass
362
+
363
+ @abstractmethod
364
+ async def get_network_response_body(self, request_id: str) -> str:
365
+ """
366
+ Gets the response body of the network request with the given request ID
367
+
368
+ :param request_id: string - network request ID
369
+ :return: str - response body of the network request
370
+ """
371
+ pass
372
+
373
+ # --------------------------- END network ---------------------------
374
+
375
+ # --------------------------- START scripts ---------------------------
376
+ @abstractmethod
377
+ async def execute_script(self, script: str) -> any:
378
+ """
379
+ Executes the JavaScript script in the context of the current page
380
+
381
+ :param script: string - JavaScript code to execute
382
+ :return:
383
+ """
384
+ pass
385
+
386
+ @abstractmethod
387
+ async def execute_cdp_cmd(self, cmd: str, params: dict) -> any:
388
+ """
389
+ Executes the Chrome DevTools Protocol command in the context of the current page
390
+
391
+ :param cmd: string - CDP command to execute
392
+ :param params: dict - parameters for the CDP command
393
+ :return:
394
+ """
395
+ pass
396
+
397
+ # --------------------------- END scripts ---------------------------
398
+
399
+ # --------------------------- START wait ---------------------------
400
+ @abstractmethod
401
+ async def element_is_present(self, selector: str, timeout: int) -> bool:
402
+ """
403
+ Checks if the element matching the selector is present
404
+
405
+ :param selector: string - CSS selector or XPath
406
+ :param timeout: int - seconds to wait for the element
407
+ :return: bool - whether the element is present
408
+ """
409
+ pass
410
+
411
+ @abstractmethod
412
+ async def element_is_visible(self, selector: str, timeout: int) -> bool:
413
+ """
414
+ Checks if the element matching the selector is visible
415
+
416
+ :param selector: string - CSS selector or XPath
417
+ :param timeout: int - seconds to wait for the element
418
+ :return: bool - whether the element is visible
419
+ """
420
+ pass
421
+
422
+ @abstractmethod
423
+ async def element_is_invisible(self, selector: str, timeout: int) -> bool:
424
+ """
425
+ Checks if the element matching the selector is invisible
426
+
427
+ :param selector: string - CSS selector or XPath
428
+ :param timeout: int - seconds to wait for the element
429
+ :return: bool - whether the element is invisible
430
+ """
431
+ pass
432
+
433
+ @abstractmethod
434
+ async def element_is_clickable(self, selector: str, timeout: int) -> bool:
435
+ """
436
+ Checks if the element matching the selector is clickable
437
+
438
+ :param selector: string - CSS selector or XPath
439
+ :param timeout: int - seconds to wait for the element
440
+ :return: bool - whether the element is clickable
441
+ """
442
+ pass
443
+
444
+ @abstractmethod
445
+ async def text_is_present(self, text: str, selector: str, timeout: int) -> bool:
446
+ """
447
+ Checks if the text is present in the element matching the selector
448
+
449
+ :param text: string - text to check
450
+ :param selector: string - CSS selector or XPath
451
+ :param timeout: int - seconds to wait for the element
452
+ :return: bool - whether the text is present in the element
453
+ """
454
+ pass
455
+
456
+ @abstractmethod
457
+ async def alert_is_present(self, timeout: int, message: str) -> bool:
458
+ """
459
+ Checks if an alert is present
460
+
461
+ :param timeout: int - seconds to wait for the alert
462
+ :param message: str - alert message
463
+ :return: bool - whether an alert is present
464
+ """
465
+ pass
466
+
467
+ @abstractmethod
468
+ async def page_is_loading(self, timeout: int) -> bool:
469
+ """
470
+ Checks if the page is ready
471
+
472
+ :param timeout: int - seconds to wait for the page to be ready
473
+ :return: bool - whether the page is ready
474
+ """
475
+ pass
476
+
477
+ # --------------------------- END wait ---------------------------
478
+
479
+ # --------------------------- START session data ---------------------------
480
+ @abstractmethod
481
+ async def get_all_cookies(self) -> list[Cookie]:
482
+ """
483
+ Gets all cookies
484
+ :return: list[Cookie]
485
+ """
486
+ pass
487
+
488
+ async def get_cookies_filter(
489
+ self, name: str | None = None, domain: str | None = None, path: str | None = None
490
+ ) -> list[Cookie]:
491
+ """
492
+ Gets cookies filtered by name, domain and path
493
+ :param name:
494
+ :param domain:
495
+ :param path:
496
+ :return:
497
+ """
498
+ cookies = await self.get_all_cookies()
499
+ return [cookie for cookie in cookies if cookie.name == name]
500
+
501
+ async def get_cookie_by_name(self, name: str) -> Cookie | None:
502
+ """
503
+ Gets a cookie by name
504
+
505
+ :param name: string - name of the cookie
506
+ :return: Cookie | None - cookie with the given name
507
+ """
508
+ cookies = await self.get_all_cookies()
509
+ for cookie in cookies:
510
+ if cookie.name == name:
511
+ return cookie
512
+ return None
513
+
514
+ @abstractmethod
515
+ async def add_cookie(self, cookie: Cookie) -> None:
516
+ """
517
+ Adds a cookie to the current session
518
+
519
+ :param cookie: Cookie - cookie to add
520
+ :return:
521
+ """
522
+ pass
523
+
524
+ async def add_cookies(self, cookies: list[Cookie]) -> None:
525
+ """
526
+ Adds multiple cookies to the current session
527
+
528
+ :param cookies: list[Cookie] - cookies to add
529
+ :return:
530
+ """
531
+ for cookie in cookies:
532
+ await self.add_cookie(cookie)
533
+
534
+ @abstractmethod
535
+ async def delete_all_cookies(self) -> None:
536
+ """
537
+ Deletes all cookies from the current session
538
+ :return:
539
+ """
540
+ pass
541
+
542
+ @abstractmethod
543
+ async def delete_cookie_by_name(self, name: str) -> None:
544
+ """
545
+ Deletes a cookie by name
546
+
547
+ :param name: string - name of the cookie to delete
548
+ :return:
549
+ """
550
+ pass
551
+
552
+ @abstractmethod
553
+ async def delete_cookie_filter(
554
+ self, name: str | None = None, domain: str | None = None, path: str | None = None
555
+ ) -> None:
556
+ """
557
+ Deletes cookies by name, domain, and path
558
+
559
+ :param name:
560
+ :param domain:
561
+ :param path:
562
+ :return:
563
+ """
564
+ pass
565
+
566
+ @abstractmethod
567
+ async def get_all_local_storage(self) -> dict:
568
+ """
569
+ Gets all local storage
570
+ :return: dict
571
+ """
572
+ pass
573
+
574
+ async def get_local_storage_filter(self, name: str) -> str | None:
575
+ """
576
+ Gets a local storage item value by name
577
+
578
+ :param name:
579
+ :return:
580
+ """
581
+ local_storage = await self.get_all_local_storage()
582
+ return local_storage.get(name, None)
583
+
584
+ # --------------------------- END session data ---------------------------
@@ -0,0 +1,10 @@
1
+ from camoufox import AsyncCamoufox
2
+ from playwright.async_api import Page
3
+
4
+ from browser_toolkit.playwright import PlaywrightTollKit
5
+
6
+
7
+ class CamoufoxTollKit(PlaywrightTollKit):
8
+ def __init__(self, browser: AsyncCamoufox, page: Page, *args, **kwargs):
9
+ self.browser: AsyncCamoufox = browser
10
+ self.page: Page = page
File without changes
@@ -0,0 +1,27 @@
1
+ from playwright.async_api import async_playwright, Browser, Page
2
+ from browser_toolkit.playwright import PlaywrightTollKit
3
+
4
+
5
+ async def get_playwright(headless: bool = False) -> tuple[Browser, Page]:
6
+ playwright = await async_playwright().start()
7
+ browser = await playwright.chromium.launch(headless=headless)
8
+ page = await browser.new_page()
9
+ return browser, page
10
+
11
+
12
+ async def get_playwright_toolkit() -> PlaywrightTollKit:
13
+ browser, page = await get_playwright()
14
+ btk = PlaywrightTollKit(
15
+ browser=browser,
16
+ page=page,
17
+ )
18
+ return btk
19
+
20
+
21
+ async def main():
22
+ async with async_playwright() as p:
23
+ browser = await p.chromium.launch()
24
+ page = await browser.new_page()
25
+ await page.goto("https://playwright.dev")
26
+ print(await page.title())
27
+ await browser.close()