diff --git a/examples/cdp_mode/ReadMe.md b/examples/cdp_mode/ReadMe.md index 37ff56aad52..79e96643291 100644 --- a/examples/cdp_mode/ReadMe.md +++ b/examples/cdp_mode/ReadMe.md @@ -465,6 +465,10 @@ sb.cdp.uncheck_if_checked(selector) sb.cdp.unselect_if_selected(selector) sb.cdp.is_element_present(selector) sb.cdp.is_element_visible(selector) +sb.cdp.is_text_visible(text, selector="body") +sb.cdp.is_exact_text_visible(text, selector="body") +sb.cdp.wait_for_text(text, selector="body", timeout=None) +sb.cdp.wait_for_text_not_visible(text, selector="body", timeout=None) sb.cdp.wait_for_element_visible(selector, timeout=None) sb.cdp.assert_element(selector, timeout=None) sb.cdp.assert_element_visible(selector, timeout=None) @@ -478,6 +482,7 @@ sb.cdp.assert_url(url) sb.cdp.assert_url_contains(substring) sb.cdp.assert_text(text, selector="html", timeout=None) sb.cdp.assert_exact_text(text, selector="html", timeout=None) +sb.cdp.assert_text_not_visible(text, selector="body", timeout=None) sb.cdp.assert_true() sb.cdp.assert_false() sb.cdp.assert_equal(first, second) @@ -506,6 +511,7 @@ element.highlight_overlay() element.mouse_click() element.mouse_drag(destination) element.mouse_move() +element.press_keys(text) element.query_selector(selector) element.querySelector(selector) element.query_selector_all(selector) diff --git a/examples/cdp_mode/raw_ahrefs.py b/examples/cdp_mode/raw_ahrefs.py new file mode 100644 index 00000000000..60f5554b9a8 --- /dev/null +++ b/examples/cdp_mode/raw_ahrefs.py @@ -0,0 +1,17 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, incognito=True, locale_code="en") as sb: + url = "https://ahrefs.com/website-authority-checker" + input_field = 'input[placeholder="Enter domain"]' + submit_button = 'span:contains("Check Authority")' + sb.activate_cdp_mode(url) # The bot-check is later + sb.type(input_field, "github.com/seleniumbase/SeleniumBase") + sb.cdp.scroll_down(36) + sb.click(submit_button) + sb.uc_gui_click_captcha() + sb.wait_for_text_not_visible("Checking", timeout=15) + sb.click_if_visible('button[data-cky-tag="close-button"]') + sb.highlight('p:contains("github.com/seleniumbase/SeleniumBase")') + sb.highlight('a:contains("Top 100 backlinks")') + sb.set_messenger_theme(location="bottom_center") + sb.post_message("SeleniumBase wasn't detected!") diff --git a/examples/cdp_mode/raw_elal.py b/examples/cdp_mode/raw_elal.py new file mode 100644 index 00000000000..f5003db4f30 --- /dev/null +++ b/examples/cdp_mode/raw_elal.py @@ -0,0 +1,55 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, locale_code="en") as sb: + url = "www.elal.com/flight-deals/en-us/flights-from-boston-to-tel-aviv" + sb.activate_cdp_mode(url) + sb.sleep(2) + sb.cdp.click('button[data-att="search"]') + sb.sleep(4) + sb.cdp.click_if_visible("#onetrust-close-btn-container button") + sb.sleep(0.5) + view_other_dates = 'button[aria-label*="viewOtherDates.cta"]' + if sb.cdp.is_element_visible(view_other_dates): + sb.cdp.click(view_other_dates) + sb.sleep(4.5) + if sb.is_element_visible("flexible-search-calendar"): + print("*** Flight Calendar for El Al (Boston to Tel Aviv): ***") + print(sb.cdp.get_text("flexible-search-calendar")) + prices = [] + elements = sb.cdp.find_elements("span.matric-cell__content__price") + if elements: + print("*** Prices List: ***") + for element in elements: + prices.append(element.text) + for price in sorted(prices): + print(price) + print("*** Lowest Price: ***") + lowest_price = sorted(prices)[0] + print(lowest_price) + sb.cdp.find_element_by_text(lowest_price).click() + sb.sleep(1) + search_cell = 'button[aria-label*="Search.cell.buttonTitle"]' + sb.cdp.scroll_into_view(search_cell) + sb.sleep(1) + sb.cdp.click(search_cell) + sb.sleep(5) + else: + elements = sb.cdp.find_elements("div.ui-bound__price__value") + print("*** Lowest Prices: ***") + first = True + for element in elements: + if "lowest price" in element.text: + if first: + print("Departure Flight:") + print(element.text) + first = False + else: + print("Return Flight:") + print(element.text) + break + dates = sb.cdp.find_elements('div[class*="flight-date"]') + if len(dates) == 2: + print("*** Departure Date: ***") + print(dates[0].text) + print("*** Return Date: ***") + print(dates[1].text) diff --git a/examples/raw_ahrefs.py b/examples/raw_ahrefs.py index 86c884ee2c3..3c8441cd4f6 100644 --- a/examples/raw_ahrefs.py +++ b/examples/raw_ahrefs.py @@ -9,7 +9,7 @@ sb.reconnect(0.1) sb.uc_click(submit_button, reconnect_time=3.25) sb.uc_gui_click_captcha() - sb.wait_for_text_not_visible("Checking", timeout=11.5) + sb.wait_for_text_not_visible("Checking", timeout=15) sb.click_if_visible('button[data-cky-tag="close-button"]') sb.highlight('p:contains("github.com/seleniumbase/SeleniumBase")') sb.highlight('a:contains("Top 100 backlinks")') diff --git a/requirements.txt b/requirements.txt index ff2bee17e98..2c05747d7f7 100755 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ attrs>=25.1.0 certifi>=2025.1.31 exceptiongroup>=1.2.2 websockets~=13.1;python_version<"3.9" -websockets>=14.2;python_version>="3.9" +websockets>=15.0;python_version>="3.9" filelock~=3.16.1;python_version<"3.9" filelock>=3.17.0;python_version>="3.9" fasteners>=0.19 @@ -38,8 +38,8 @@ sniffio==1.3.1 h11==0.14.0 outcome==1.3.0.post0 trio==0.27.0;python_version<"3.9" -trio==0.28.0;python_version>="3.9" -trio-websocket==0.11.1 +trio==0.29.0;python_version>="3.9" +trio-websocket==0.12.1 wsproto==1.2.0 websocket-client==1.8.0 selenium==4.27.1;python_version<"3.9" @@ -74,7 +74,7 @@ coverage>=7.6.12;python_version>="3.9" pytest-cov>=5.0.0;python_version<"3.9" pytest-cov>=6.0.0;python_version>="3.9" flake8==5.0.4;python_version<"3.9" -flake8==7.1.1;python_version>="3.9" +flake8==7.1.2;python_version>="3.9" mccabe==0.7.0 pyflakes==2.5.0;python_version<"3.9" pyflakes==3.2.0;python_version>="3.9" diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index 243494fa363..a304fe8573d 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.34.15" +__version__ = "4.34.16" diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index 86f5f428071..4fac3296817 100644 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -718,6 +718,10 @@ def uc_open_with_cdp_mode(driver, url=None): cdp.is_selected = CDPM.is_selected cdp.is_element_present = CDPM.is_element_present cdp.is_element_visible = CDPM.is_element_visible + cdp.is_text_visible = CDPM.is_text_visible + cdp.is_exact_text_visible = CDPM.is_exact_text_visible + cdp.wait_for_text = CDPM.wait_for_text + cdp.wait_for_text_not_visible = CDPM.wait_for_text_not_visible cdp.wait_for_element_visible = CDPM.wait_for_element_visible cdp.assert_element = CDPM.assert_element cdp.assert_element_visible = CDPM.assert_element_visible @@ -731,6 +735,7 @@ def uc_open_with_cdp_mode(driver, url=None): cdp.assert_url_contains = CDPM.assert_url_contains cdp.assert_text = CDPM.assert_text cdp.assert_exact_text = CDPM.assert_exact_text + cdp.assert_text_not_visible = CDPM.assert_text_not_visible cdp.assert_true = CDPM.assert_true cdp.assert_false = CDPM.assert_false cdp.assert_equal = CDPM.assert_equal @@ -2280,6 +2285,7 @@ def _set_chrome_options( or proxy_string ): chrome_options.add_argument("--ignore-certificate-errors") + chrome_options.add_argument("--ignore-ssl-errors=yes") if not enable_ws: chrome_options.add_argument("--disable-web-security") if ( @@ -4231,6 +4237,7 @@ def get_local_driver( edge_options.add_argument("--log-level=3") edge_options.add_argument("--no-first-run") edge_options.add_argument("--ignore-certificate-errors") + edge_options.add_argument("--ignore-ssl-errors=yes") if devtools and not headless: edge_options.add_argument("--auto-open-devtools-for-tabs") edge_options.add_argument("--allow-file-access-from-files") diff --git a/seleniumbase/core/sb_cdp.py b/seleniumbase/core/sb_cdp.py index 2a87ec0a389..59a6a7e5328 100644 --- a/seleniumbase/core/sb_cdp.py +++ b/seleniumbase/core/sb_cdp.py @@ -62,6 +62,7 @@ def __add_sync_methods(self, element): lambda destination: self.__mouse_drag(element, destination) ) element.mouse_move = lambda: self.__mouse_move(element) + element.press_keys = lambda text: self.__press_keys(element, text) element.query_selector = ( lambda selector: self.__query_selector(element, selector) ) @@ -211,7 +212,8 @@ def find_element_by_text(self, text, tag_name=None, timeout=None): element = self.__add_sync_methods(element.parent) return self.__add_sync_methods(element) elif ( - element.parent.parent + element.parent + and element.parent.parent and tag_name in element.parent.parent.tag_name.lower() and text.strip() in element.parent.parent.text ): @@ -272,7 +274,8 @@ def find_elements_by_text(self, text, tag_name=None): if element not in updated_elements: updated_elements.append(element) elif ( - element.parent.parent + element.parent + and element.parent.parent and tag_name in element.parent.parent.tag_name.lower() and text.strip() in element.parent.parent.text ): @@ -445,6 +448,23 @@ def __mouse_move(self, element): self.loop.run_until_complete(element.mouse_move_async()) ) + def __press_keys(self, element, text): + element.scroll_into_view() + submit = False + if text.endswith("\n") or text.endswith("\r"): + submit = True + text = text[:-1] + for key in text: + element.send_keys(key) + time.sleep(0.044) + if submit: + element.send_keys("\r\n") + time.sleep(0.044) + self.__slow_mode_pause_if_set() + return ( + self.loop.run_until_complete(self.page.wait()) + ) + def __query_selector(self, element, selector): selector = self.__convert_to_css_if_xpath(selector) element2 = self.loop.run_until_complete( @@ -1681,21 +1701,79 @@ def is_element_visible(self, selector): return True return False - def wait_for_element_visible(self, selector, timeout=None): + def is_text_visible(self, text, selector="body"): + selector = self.__convert_to_css_if_xpath(selector) + text = text.strip() + element = None + try: + element = self.find_element(selector, timeout=0.1) + except Exception: + return False + with suppress(Exception): + if text in element.text_all: + return True + return False + + def is_exact_text_visible(self, text, selector="body"): + selector = self.__convert_to_css_if_xpath(selector) + text = text.strip() + element = None + try: + element = self.find_element(selector, timeout=0.1) + except Exception: + return False + with suppress(Exception): + if text == element.text_all.strip(): + return True + return False + + def wait_for_text(self, text, selector="body", timeout=None): if not timeout: timeout = settings.SMALL_TIMEOUT + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) + text = text.strip() + element = None try: - self.select(selector, timeout=timeout) + element = self.find_element(selector, timeout=timeout) except Exception: - raise Exception("Element {%s} was not found!" % selector) - for i in range(30): - if self.is_element_visible(selector): - return self.select(selector) + raise Exception("Element {%s} not found!" % selector) + for i in range(int(timeout * 10)): + with suppress(Exception): + element = self.find_element(selector, timeout=0.1) + if text in element.text_all: + return True + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break time.sleep(0.1) - raise Exception("Element {%s} was not visible!" % selector) + raise Exception( + "Text {%s} not found in {%s}! Actual text: {%s}" + % (text, selector, element.text_all) + ) - def assert_element(self, selector, timeout=None): - """Same as assert_element_visible()""" + def wait_for_text_not_visible(self, text, selector="body", timeout=None): + if not timeout: + timeout = settings.SMALL_TIMEOUT + text = text.strip() + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) + for i in range(int(timeout * 10)): + if not self.is_text_visible(text, selector): + return True + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(0.1) + plural = "s" + if timeout == 1: + plural = "" + raise Exception( + "Text {%s} in {%s} was still visible after %s second%s!" + % (text, selector, timeout, plural) + ) + + def wait_for_element_visible(self, selector, timeout=None): if not timeout: timeout = settings.SMALL_TIMEOUT try: @@ -1704,10 +1782,15 @@ def assert_element(self, selector, timeout=None): raise Exception("Element {%s} was not found!" % selector) for i in range(30): if self.is_element_visible(selector): - return True + return self.select(selector) time.sleep(0.1) raise Exception("Element {%s} was not visible!" % selector) + def assert_element(self, selector, timeout=None): + """Same as assert_element_visible()""" + self.assert_element_visible(selector, timeout=timeout) + return True + def assert_element_visible(self, selector, timeout=None): """Same as assert_element()""" if not timeout: @@ -1852,29 +1935,9 @@ def assert_url_contains(self, substring): raise Exception(error % (expected, actual)) def assert_text(self, text, selector="body", timeout=None): - if not timeout: - timeout = settings.SMALL_TIMEOUT - start_ms = time.time() * 1000.0 - stop_ms = start_ms + (timeout * 1000.0) - text = text.strip() - element = None - try: - element = self.find_element(selector, timeout=timeout) - except Exception: - raise Exception("Element {%s} not found!" % selector) - for i in range(int(timeout * 10)): - with suppress(Exception): - element = self.find_element(selector, timeout=0.1) - if text in element.text_all: - return True - now_ms = time.time() * 1000.0 - if now_ms >= stop_ms: - break - time.sleep(0.1) - raise Exception( - "Text {%s} not found in {%s}! Actual text: {%s}" - % (text, selector, element.text_all) - ) + """Same as wait_for_text()""" + self.wait_for_text(text, selector=selector, timeout=timeout) + return True def assert_exact_text(self, text, selector="body", timeout=None): if not timeout: @@ -1904,6 +1967,13 @@ def assert_exact_text(self, text, selector="body", timeout=None): % (text, element.text_all, selector) ) + def assert_text_not_visible(self, text, selector="body", timeout=None): + """Raises an exception if the text is still visible after timeout.""" + self.wait_for_text_not_visible( + text, selector=selector, timeout=timeout + ) + return True + def assert_true(self, expression): if not expression: raise AssertionError("%s is not true" % expression) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index dca2b26e781..8c6c57d0ca6 100644 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -1454,6 +1454,8 @@ def is_element_enabled(self, selector, by="css selector"): def is_text_visible(self, text, selector="body", by="css selector"): """Returns whether the text substring is visible in the element.""" + if self.__is_cdp_swap_needed(): + return self.cdp.is_text_visible(text, selector) self.wait_for_ready_state_complete() time.sleep(0.01) selector, by = self.__recalculate_selector(selector, by) @@ -1464,6 +1466,8 @@ def is_text_visible(self, text, selector="body", by="css selector"): def is_exact_text_visible(self, text, selector="body", by="css selector"): """Returns whether the text is exactly equal to the element text. (Leading and trailing whitespace is ignored in the verification.)""" + if self.__is_cdp_swap_needed(): + return self.cdp.is_exact_text_visible(text, selector) self.wait_for_ready_state_complete() time.sleep(0.01) selector, by = self.__recalculate_selector(selector, by) @@ -9281,7 +9285,8 @@ def set_messenger_theme( "bottom_left", "bottom_center", "bottom_right"] max_messages: The limit of concurrent messages to display.""" self.__check_scope() - self._check_browser() + if not self.__is_cdp_swap_needed(): + self._check_browser() if not theme: theme = "default" # "flat" if not location: @@ -9308,7 +9313,8 @@ def post_message(self, message, duration=None, pause=True, style="info"): You can also post messages by using => self.execute_script('Messenger().post("My Message")') """ self.__check_scope() - self._check_browser() + if not self.__is_cdp_swap_needed(): + self._check_browser() if style not in ["info", "success", "error"]: style = "info" if not duration: @@ -10326,6 +10332,10 @@ def wait_for_text_not_visible( if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: timeout = self.__get_new_timeout(timeout) selector, by = self.__recalculate_selector(selector, by) + if self.__is_cdp_swap_needed(): + return self.cdp.wait_for_text( + text, selector=selector, timeout=timeout + ) return page_actions.wait_for_text_not_visible( self.driver, text, selector, by, timeout ) @@ -13909,7 +13919,8 @@ def __highlight_element_with_js(self, element, loops, o_bs): js_utils.highlight_element_with_js(self.driver, element, loops, o_bs) def __highlight_with_jquery(self, selector, loops, o_bs): - self.wait_for_ready_state_complete() + if not self.__is_cdp_swap_needed(): + self.wait_for_ready_state_complete() js_utils.highlight_with_jquery(self.driver, selector, loops, o_bs) def __highlight_with_js_2(self, message, selector, o_bs): diff --git a/seleniumbase/fixtures/js_utils.py b/seleniumbase/fixtures/js_utils.py index 2e0ef7ef112..52a6e0cad78 100644 --- a/seleniumbase/fixtures/js_utils.py +++ b/seleniumbase/fixtures/js_utils.py @@ -585,52 +585,60 @@ def highlight_with_jquery(driver, selector, loops=4, o_bs=""): '0px 0px 6px 6px rgba(128, 128, 128, 0.5)');""" % selector ) - safe_execute_script(driver, script) + with suppress(Exception): + safe_execute_script(driver, script) for n in range(loops): script = ( """jQuery('%s').css('box-shadow', '0px 0px 6px 6px rgba(255, 0, 0, 1)');""" % selector ) - execute_script(driver, script) + with suppress(Exception): + execute_script(driver, script) time.sleep(0.0181) script = ( """jQuery('%s').css('box-shadow', '0px 0px 6px 6px rgba(128, 0, 128, 1)');""" % selector ) - execute_script(driver, script) + with suppress(Exception): + execute_script(driver, script) time.sleep(0.0181) script = ( """jQuery('%s').css('box-shadow', '0px 0px 6px 6px rgba(0, 0, 255, 1)');""" % selector ) - execute_script(driver, script) + with suppress(Exception): + execute_script(driver, script) time.sleep(0.0181) script = ( """jQuery('%s').css('box-shadow', '0px 0px 6px 6px rgba(0, 255, 0, 1)');""" % selector ) - execute_script(driver, script) + with suppress(Exception): + execute_script(driver, script) time.sleep(0.0181) script = ( """jQuery('%s').css('box-shadow', '0px 0px 6px 6px rgba(128, 128, 0, 1)');""" % selector ) - execute_script(driver, script) + with suppress(Exception): + execute_script(driver, script) time.sleep(0.0181) script = ( """jQuery('%s').css('box-shadow', '0px 0px 6px 6px rgba(128, 0, 128, 1)');""" % selector ) - execute_script(driver, script) + with suppress(Exception): + execute_script(driver, script) time.sleep(0.0181) script = """jQuery('%s').css('box-shadow', '%s');""" % (selector, o_bs) - execute_script(driver, script) + with suppress(Exception): + execute_script(driver, script) def add_css_link(driver, css_link): @@ -924,9 +932,20 @@ def post_message(driver, message, msg_dur=None, style="info"): """hideAfter: %s, hideOnNavigate: true});""" % (message, style, msg_dur) ) + retry = False try: execute_script(driver, messenger_script) + except TypeError as e: + if ( + shared_utils.is_cdp_swap_needed(driver) + and "cannot unpack non-iterable" in str(e) + ): + pass + else: + retry = True except Exception: + retry = True + if retry: activate_messenger(driver) set_messenger_theme(driver) try: @@ -1273,7 +1292,10 @@ def slow_scroll_to_element(driver, element, *args, **kwargs): scroll_position = execute_script(driver, "return window.scrollY;") element_location_y = None try: - element_location_y = element.location["y"] + if shared_utils.is_cdp_swap_needed(driver): + element.get_position().y + else: + element_location_y = element.location["y"] except Exception: element.location_once_scrolled_into_view return diff --git a/seleniumbase/undetected/cdp_driver/browser.py b/seleniumbase/undetected/cdp_driver/browser.py index 3ed79233cdc..04c2de219b8 100644 --- a/seleniumbase/undetected/cdp_driver/browser.py +++ b/seleniumbase/undetected/cdp_driver/browser.py @@ -265,6 +265,8 @@ async def get( :param new_window: Open new window :return: Page """ + if url and ":" not in url: + url = "https://" + url if new_tab or new_window: # Create new target using the browser session. target_id = await self.connection.send( diff --git a/seleniumbase/undetected/cdp_driver/cdp_util.py b/seleniumbase/undetected/cdp_driver/cdp_util.py index 1f602a14af3..a39f0c5d883 100644 --- a/seleniumbase/undetected/cdp_driver/cdp_util.py +++ b/seleniumbase/undetected/cdp_driver/cdp_util.py @@ -4,6 +4,7 @@ import fasteners import logging import os +import sys import time import types import typing @@ -11,6 +12,7 @@ from seleniumbase import config as sb_config from seleniumbase.config import settings from seleniumbase.core import detect_b_ver +from seleniumbase.core import proxy_helper from seleniumbase.fixtures import constants from seleniumbase.fixtures import shared_utils from typing import Optional, List, Union, Callable @@ -23,6 +25,7 @@ logger = logging.getLogger(__name__) IS_LINUX = shared_utils.is_linux() +PROXY_DIR_LOCK = proxy_helper.PROXY_DIR_LOCK T = typing.TypeVar("T") @@ -139,6 +142,85 @@ def __activate_virtual_display_as_needed( __activate_standard_virtual_display() +def __set_proxy_filenames(): + DOWNLOADS_DIR = constants.Files.DOWNLOADS_FOLDER + for num in range(1000): + PROXY_DIR_PATH = os.path.join(DOWNLOADS_DIR, "proxy_ext_dir_%s" % num) + if os.path.exists(PROXY_DIR_PATH): + continue + proxy_helper.PROXY_DIR_PATH = PROXY_DIR_PATH + return + # Exceeded upper bound. Use Defaults: + PROXY_DIR_PATH = os.path.join(DOWNLOADS_DIR, "proxy_ext_dir") + proxy_helper.PROXY_DIR_PATH = PROXY_DIR_PATH + + +def __add_chrome_ext_dir(extension_dir, dir_path): + # Add dir_path to the existing extension_dir + option_exists = False + if extension_dir: + option_exists = True + extension_dir = "%s,%s" % ( + extension_dir, os.path.realpath(dir_path) + ) + if not option_exists: + extension_dir = os.path.realpath(dir_path) + return extension_dir + + +def __add_chrome_proxy_extension( + extension_dir, + proxy_string, + proxy_user, + proxy_pass, + proxy_bypass_list=None, + multi_proxy=False, +): + """Implementation of https://stackoverflow.com/a/35293284/7058266 + for https://stackoverflow.com/q/12848327/7058266 + (Run Selenium on a proxy server that requires authentication.)""" + args = " ".join(sys.argv) + bypass_list = proxy_bypass_list + if ( + not ("-n" in sys.argv or " -n=" in args or args == "-c") + and not multi_proxy + ): + # Single-threaded + proxy_dir_lock = fasteners.InterProcessLock(PROXY_DIR_LOCK) + with proxy_dir_lock: + proxy_helper.create_proxy_ext( + proxy_string, + proxy_user, + proxy_pass, + bypass_list, + zip_it=False, + ) + proxy_dir_path = proxy_helper.PROXY_DIR_PATH + extension_dir = __add_chrome_ext_dir( + extension_dir, proxy_dir_path + ) + else: + # Multi-threaded + proxy_dir_lock = fasteners.InterProcessLock(PROXY_DIR_LOCK) + with proxy_dir_lock: + with suppress(Exception): + shared_utils.make_writable(PROXY_DIR_LOCK) + if multi_proxy: + __set_proxy_filenames() + if not os.path.exists(proxy_helper.PROXY_DIR_PATH): + proxy_helper.create_proxy_ext( + proxy_string, + proxy_user, + proxy_pass, + bypass_list, + zip_it=False, + ) + extension_dir = __add_chrome_ext_dir( + extension_dir, proxy_helper.PROXY_DIR_PATH + ) + return extension_dir + + async def start( config: Optional[Config] = None, *, @@ -156,6 +238,8 @@ async def start( xvfb: Optional[int] = None, # Use a special virtual display on Linux headed: Optional[bool] = None, # Override default Xvfb mode on Linux expert: Optional[bool] = None, # Open up closed Shadow-root elements + proxy: Optional[str] = None, # "host:port" or "user:pass@host:port" + extension_dir: Optional[str] = None, # Chrome extension directory **kwargs: Optional[dict], ) -> Browser: """ @@ -200,6 +284,18 @@ async def start( if IS_LINUX and not headless and not headed and not xvfb: xvfb = True # The default setting on Linux __activate_virtual_display_as_needed(headless, headed, xvfb, xvfb_metrics) + if proxy and "@" in str(proxy): + user_with_pass = proxy.split("@")[0] + if ":" in user_with_pass: + proxy_user = user_with_pass.split(":")[0] + proxy_pass = user_with_pass.split(":")[1] + proxy_string = proxy.split("@")[1] + extension_dir = __add_chrome_proxy_extension( + extension_dir, + proxy_string, + proxy_user, + proxy_pass, + ) if not config: config = Config( user_data_dir, @@ -213,13 +309,19 @@ async def start( host=host, port=port, expert=expert, + proxy=proxy, + extension_dir=extension_dir, **kwargs, ) + driver = None try: - return await Browser.create(config) + driver = await Browser.create(config) except Exception: time.sleep(0.15) - return await Browser.create(config) + driver = await Browser.create(config) + if proxy and "@" in str(proxy): + time.sleep(0.11) + return driver async def start_async(*args, **kwargs) -> Browser: diff --git a/seleniumbase/undetected/cdp_driver/config.py b/seleniumbase/undetected/cdp_driver/config.py index 85d4294b02d..b11228ea59d 100644 --- a/seleniumbase/undetected/cdp_driver/config.py +++ b/seleniumbase/undetected/cdp_driver/config.py @@ -40,6 +40,8 @@ def __init__( host: str = AUTO, port: int = AUTO, expert: bool = AUTO, + proxy: Optional[str] = None, + extension_dir: Optional[str] = None, **kwargs: dict, ): """ @@ -91,6 +93,8 @@ def __init__( self.host = host self.port = port self.expert = expert + self.proxy = proxy + self.extension_dir = extension_dir self._extensions = [] # When using posix-ish operating system and running as root, # you must use no_sandbox=True @@ -195,6 +199,12 @@ def __call__(self): "--disable-web-security", "--disable-site-isolation-trials", ] + if self.proxy: + args.append("--proxy-server=%s" % self.proxy.split("@")[-1]) + args.append("--ignore-certificate-errors") + args.append("--ignore-ssl-errors=yes") + if self.extension_dir: + args.append("--load-extension=%s" % self.extension_dir) if self._browser_args: args.extend([arg for arg in self._browser_args if arg not in args]) if self.headless: diff --git a/setup.py b/setup.py index c69e13ae49a..76871d7666f 100755 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ print("\nERROR! Publishing to PyPI requires Python>=3.9") sys.exit() print("\n*** Checking code health with flake8:\n") - os.system("python -m pip install 'flake8==7.1.1'") + os.system("python -m pip install 'flake8==7.1.2'") flake8_status = os.system("flake8 --exclude=recordings,temp") if flake8_status != 0: print("\nERROR! Fix flake8 issues before publishing to PyPI!\n") @@ -156,7 +156,7 @@ "certifi>=2025.1.31", "exceptiongroup>=1.2.2", 'websockets~=13.1;python_version<"3.9"', - 'websockets>=14.2;python_version>="3.9"', + 'websockets>=15.0;python_version>="3.9"', 'filelock~=3.16.1;python_version<"3.9"', 'filelock>=3.17.0;python_version>="3.9"', 'fasteners>=0.19', @@ -187,8 +187,8 @@ 'h11==0.14.0', 'outcome==1.3.0.post0', 'trio==0.27.0;python_version<"3.9"', - 'trio==0.28.0;python_version>="3.9"', - 'trio-websocket==0.11.1', + 'trio==0.29.0;python_version>="3.9"', + 'trio-websocket==0.12.1', 'wsproto==1.2.0', 'websocket-client==1.8.0', 'selenium==4.27.1;python_version<"3.9"', @@ -236,7 +236,7 @@ # Usage: flake8 "flake8": [ 'flake8==5.0.4;python_version<"3.9"', - 'flake8==7.1.1;python_version>="3.9"', + 'flake8==7.1.2;python_version>="3.9"', "mccabe==0.7.0", 'pyflakes==2.5.0;python_version<"3.9"', 'pyflakes==3.2.0;python_version>="3.9"',