diff --git a/examples/raw_gui_click.py b/examples/raw_gui_click.py
new file mode 100644
index 00000000000..768da6d0bbc
--- /dev/null
+++ b/examples/raw_gui_click.py
@@ -0,0 +1,19 @@
+"""
+UC Mode now has uc_gui_click_cf(), which uses PyAutoGUI.
+An incomplete UserAgent is used to force CAPTCHA-solving.
+"""
+import sys
+from seleniumbase import SB
+
+agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/126.0.0.0"
+if "linux" in sys.platform:
+ agent = None # Use the default UserAgent
+
+with SB(uc=True, test=True, rtf=True, agent=agent) as sb:
+ url = "https://www.virtualmanager.com/en/login"
+ sb.uc_open_with_reconnect(url, 4)
+ sb.uc_gui_click_cf() # Ready if needed!
+ sb.assert_element('input[name*="email"]')
+ sb.assert_element('input[name*="login"]')
+ sb.set_messenger_theme(location="bottom_center")
+ sb.post_message("SeleniumBase wasn't detected!")
diff --git a/examples/raw_order_tickets.py b/examples/raw_order_tickets.py
index 9dd64776309..43775eebd63 100644
--- a/examples/raw_order_tickets.py
+++ b/examples/raw_order_tickets.py
@@ -1,11 +1,14 @@
from seleniumbase import SB
-with SB(uc=True, test=True, ad_block_on=True) as sb:
+with SB(uc=True, test=True, ad_block=True) as sb:
url = "https://www.thaiticketmajor.com/concert/"
- sb.driver.uc_open_with_reconnect(url, 6.111)
- sb.driver.uc_click("button.btn-signin", 4.1)
+ sb.uc_open_with_reconnect(url, 6.111)
+ sb.uc_click("button.btn-signin", 4.1)
sb.switch_to_frame('iframe[title*="Cloudflare"]')
- sb.assert_element("div#success svg#success-icon")
+ if not sb.is_element_visible("svg#success-icon"):
+ sb.uc_gui_handle_cf()
+ sb.switch_to_frame('iframe[title*="Cloudflare"]')
+ sb.assert_element("svg#success-icon")
sb.switch_to_default_content()
sb.set_messenger_theme(location="top_center")
sb.post_message("SeleniumBase wasn't detected!")
diff --git a/examples/raw_uc_mode.py b/examples/raw_uc_mode.py
index 560cba94719..c0cd257486e 100644
--- a/examples/raw_uc_mode.py
+++ b/examples/raw_uc_mode.py
@@ -3,9 +3,8 @@
with SB(uc=True, test=True) as sb:
url = "https://gitlab.com/users/sign_in"
- sb.driver.uc_open_with_reconnect(url, 3)
- if not sb.is_text_visible("Username", '[for="user_login"]'):
- sb.driver.uc_open_with_reconnect(url, 4)
+ sb.uc_open_with_reconnect(url, 4)
+ sb.uc_gui_click_cf()
sb.assert_text("Username", '[for="user_login"]', timeout=3)
sb.assert_element('label[for="user_login"]')
sb.highlight('button:contains("Sign in")')
diff --git a/help_docs/method_summary.md b/help_docs/method_summary.md
index d90701cbd90..557d913b52e 100644
--- a/help_docs/method_summary.md
+++ b/help_docs/method_summary.md
@@ -223,10 +223,22 @@ self.execute_async_script(script, timeout=None)
self.safe_execute_script(script, *args, **kwargs)
+self.get_gui_element_rect(selector, by="css selector")
+
+self.get_gui_element_center(selector, by="css selector")
+
+self.get_window_rect()
+
+self.get_window_size()
+
+self.get_window_position()
+
self.set_window_rect(x, y, width, height)
self.set_window_size(width, height)
+self.set_window_position(x, y)
+
self.maximize_window()
self.switch_to_frame(frame="iframe", timeout=None)
@@ -1062,6 +1074,10 @@ driver.uc_gui_press_keys(keys) # Use PyAutoGUI to press a list of keys
driver.uc_gui_write(text) # Similar to uc_gui_press_keys(), but faster
+driver.uc_gui_click_x_y(x, y, timeframe=0.25) # PyAutoGUI click screen
+
+driver.uc_gui_click_cf(frame="iframe", retry=False, blind=False) # (*)
+
driver.uc_gui_handle_cf(frame="iframe") # PyAutoGUI click CF Turnstile
driver.uc_switch_to_frame(frame="iframe") # Stealthy switch_to_frame()
diff --git a/help_docs/uc_mode.md b/help_docs/uc_mode.md
index b89ba282f83..e6155d0caf9 100644
--- a/help_docs/uc_mode.md
+++ b/help_docs/uc_mode.md
@@ -42,7 +42,7 @@ from seleniumbase import SB
with SB(uc=True) as sb:
url = "https://gitlab.com/users/sign_in"
- sb.driver.uc_open_with_reconnect(url, 3)
+ sb.uc_open_with_reconnect(url, 3)
```
👤 Here's a longer example, which includes a retry if the CAPTCHA isn't bypassed on the first attempt:
@@ -52,9 +52,9 @@ from seleniumbase import SB
with SB(uc=True, test=True) as sb:
url = "https://gitlab.com/users/sign_in"
- sb.driver.uc_open_with_reconnect(url, 3)
+ sb.uc_open_with_reconnect(url, 3)
if not sb.is_text_visible("Username", '[for="user_login"]'):
- sb.driver.uc_open_with_reconnect(url, 4)
+ sb.uc_open_with_reconnect(url, 4)
sb.assert_text("Username", '[for="user_login"]', timeout=3)
sb.highlight('label[for="user_login"]', loops=3)
sb.post_message("SeleniumBase wasn't detected", duration=4)
@@ -78,6 +78,8 @@ with SB(uc=True, test=True) as sb:
+If running on a Linux server, `uc_gui_handle_cf()` might not be good enough. Switch to `uc_gui_click_cf()` to be more stealthy.
+
👤 Here's an example where the CAPTCHA appears after submitting a form:
```python
@@ -87,10 +89,10 @@ with SB(uc=True, test=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.driver.uc_open_with_reconnect(url, 1) # The bot-check is later
+ sb.uc_open_with_reconnect(url, 1) # The bot-check is later
sb.type(input_field, "github.com/seleniumbase/SeleniumBase")
- sb.driver.reconnect(0.1)
- sb.driver.uc_click(submit_button, reconnect_time=4)
+ sb.reconnect(0.1)
+ sb.uc_click(submit_button, reconnect_time=4)
sb.wait_for_text_not_visible("Checking", timeout=10)
sb.highlight('p:contains("github.com/seleniumbase/SeleniumBase")')
sb.highlight('a:contains("Top 100 backlinks")')
@@ -105,10 +107,10 @@ with SB(uc=True, test=True, locale_code="en") as sb:
```python
from seleniumbase import SB
-with SB(uc=True, test=True, ad_block_on=True) as sb:
+with SB(uc=True, test=True, ad_block=True) as sb:
url = "https://www.thaiticketmajor.com/concert/"
- sb.driver.uc_open_with_reconnect(url, 5.5)
- sb.driver.uc_click("button.btn-signin", 4)
+ sb.uc_open_with_reconnect(url, 5.5)
+ sb.uc_click("button.btn-signin", 4)
sb.switch_to_frame('iframe[title*="Cloudflare"]')
sb.assert_element("div#success svg#success-icon")
sb.switch_to_default_content()
@@ -118,7 +120,7 @@ with SB(uc=True, test=True, ad_block_on=True) as sb:
-👤 On Linux, use `sb.uc_gui_handle_cf()` to handle Cloudflare Turnstiles:
+👤 On Linux, use `sb.uc_gui_click_cf()` to handle Cloudflare Turnstiles:
```python
from seleniumbase import SB
@@ -127,7 +129,7 @@ with SB(uc=True, test=True) as sb:
url = "https://www.virtualmanager.com/en/login"
sb.uc_open_with_reconnect(url, 4)
print(sb.get_page_title())
- sb.uc_gui_handle_cf() # Ready if needed!
+ sb.uc_gui_click_cf() # Ready if needed!
print(sb.get_page_title())
sb.assert_element('input[name*="email"]')
sb.assert_element('input[name*="login"]')
@@ -135,7 +137,7 @@ with SB(uc=True, test=True) as sb:
sb.post_message("SeleniumBase wasn't detected!")
```
-
+
The 2nd `print()` should output "Virtual Manager", which means that the automation successfully passed the Turnstile.
@@ -188,6 +190,10 @@ driver.uc_gui_press_keys(keys)
driver.uc_gui_write(text)
+driver.uc_gui_click_x_y(x, y, timeframe=0.25)
+
+driver.uc_gui_click_cf(frame="iframe", retry=False, blind=False)
+
driver.uc_gui_handle_cf(frame="iframe")
driver.uc_switch_to_frame(frame, reconnect_time=None)
@@ -225,7 +231,9 @@ driver.reconnect("breakpoint")
(Note that while the special UC Mode
breakpoint is active, you can't use Selenium
commands in the browser, and the browser can't detect Selenium
.)
-👤 On Linux, you may need to use `driver.uc_gui_handle_cf()` to successfully bypass a Cloudflare CAPTCHA. If there's more than one iframe on that website (and Cloudflare isn't the first one) then put the CSS Selector of that iframe as the first arg to `driver.uc_gui_handle_cf()`. This method uses `pyautogui`. In order for `pyautogui` to focus on the correct element, use `xvfb=True` / `--xvfb` to activate a special virtual display on Linux.
+👤 On Linux, you may need to use `driver.uc_gui_click_cf()` to successfully bypass a Cloudflare CAPTCHA. If there's more than one iframe on that website (and Cloudflare isn't the first one) then put the CSS Selector of that iframe as the first arg to `driver.uc_gui_click_cf()`. This method uses `pyautogui`. In order for `pyautogui` to focus on the correct element, use `xvfb=True` / `--xvfb` to activate a special virtual display on Linux.
+
+👤 `driver.uc_gui_click_cf(frame="iframe", retry=False, blind=False)` has three args. (All optional). The first one, `frame`, lets you specify the iframe in case the CAPTCHA is not located in the first iframe on the page. The second one, `retry`, lets you retry the click after reloading the page if the first one didn't work (and a CAPTCHA is still present after the page reload). The third arg, `blind`, will retry after a page reload (if the first click failed) by clicking at the last known coordinates of the CAPTCHA checkbox without confirming first with Selenium that a CAPTCHA is still on the page.
👤 To find out if UC Mode will work at all on a specific site (before adjusting for timing), load your site with the following script:
diff --git a/mkdocs_build/requirements.txt b/mkdocs_build/requirements.txt
index 73f59a3b895..eaf0295a961 100644
--- a/mkdocs_build/requirements.txt
+++ b/mkdocs_build/requirements.txt
@@ -20,7 +20,7 @@ lxml==5.2.2
pyquery==2.0.0
readtime==3.0.0
mkdocs==1.6.0
-mkdocs-material==9.5.27
+mkdocs-material==9.5.28
mkdocs-exclude-search==0.6.6
mkdocs-simple-hooks==0.1.5
mkdocs-material-extensions==1.3.1
diff --git a/requirements.txt b/requirements.txt
index 9cddf0cc5c3..f9e49ca33ff 100755
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,11 +3,11 @@ pip>=24.1.1;python_version>="3.8"
packaging>=24.0;python_version<"3.8"
packaging>=24.1;python_version>="3.8"
setuptools>=68.0.0;python_version<"3.8"
-setuptools>=70.1.1;python_version>="3.8"
+setuptools>=70.2.0;python_version>="3.8"
wheel>=0.42.0;python_version<"3.8"
wheel>=0.43.0;python_version>="3.8"
attrs>=23.2.0
-certifi>=2024.6.2
+certifi>=2024.7.4
exceptiongroup>=1.2.1
filelock>=3.12.2;python_version<"3.8"
filelock>=3.15.4;python_version>="3.8"
@@ -29,7 +29,7 @@ sniffio==1.3.1
h11==0.14.0
outcome==1.3.0.post0
trio==0.22.2;python_version<"3.8"
-trio==0.25.1;python_version>="3.8"
+trio==0.26.0;python_version>="3.8"
trio-websocket==0.11.1
wsproto==1.2.0
websocket-client==1.8.0;python_version>="3.8"
diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py
index 4b67644ca06..e965f8ed76b 100755
--- a/seleniumbase/__version__.py
+++ b/seleniumbase/__version__.py
@@ -1,2 +1,2 @@
# seleniumbase package
-__version__ = "4.28.3"
+__version__ = "4.28.4"
diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py
index 79afe2ce8b1..687e5f0ece5 100644
--- a/seleniumbase/core/browser_launcher.py
+++ b/seleniumbase/core/browser_launcher.py
@@ -649,12 +649,144 @@ def uc_gui_write(driver, text):
pyautogui.write(text)
-def uc_gui_handle_cf(driver, frame="iframe"):
+def get_gui_element_position(driver, selector):
+ element = driver.wait_for_element_present(selector, timeout=3)
+ element_rect = element.rect
+ window_rect = driver.get_window_rect()
+ window_bottom_y = window_rect["y"] + window_rect["height"]
+ viewport_height = driver.execute_script("return window.innerHeight;")
+ viewport_x = window_rect["x"] + element_rect["x"]
+ viewport_y = window_bottom_y - viewport_height + element_rect["y"]
+ return (viewport_x, viewport_y)
+
+
+def uc_gui_click_x_y(driver, x, y, timeframe=0.25, uc_lock=True):
+ install_pyautogui_if_missing(driver)
+ import pyautogui
+ pyautogui = get_configured_pyautogui(pyautogui)
+ screen_width, screen_height = pyautogui.size()
+ if x > screen_width or y > screen_height:
+ raise Exception(
+ "PyAutoGUI cannot click on point (%s, %s)"
+ " outside screen. (Width: %s, Height: %s)"
+ % (x, y, screen_width, screen_height)
+ )
+ if uc_lock:
+ gui_lock = fasteners.InterProcessLock(
+ constants.MultiBrowser.PYAUTOGUILOCK
+ )
+ with gui_lock: # Prevent issues with multiple processes
+ pyautogui.moveTo(x, y, timeframe, pyautogui.easeOutQuad)
+ if timeframe >= 0.25:
+ time.sleep(0.0555) # Wait if moving at human-speed
+ if "--debug" in sys.argv:
+ print(" pyautogui.click(%s, %s)" % (x, y))
+ pyautogui.click(x=x, y=y)
+ else:
+ # Called from a method where the gui_lock is already active
+ pyautogui.moveTo(x, y, timeframe, pyautogui.easeOutQuad)
+ if timeframe >= 0.25:
+ time.sleep(0.0555) # Wait if moving at human-speed
+ if "--debug" in sys.argv:
+ print(" pyautogui.click(%s, %s)" % (x, y))
+ pyautogui.click(x=x, y=y)
+
+
+def on_a_cf_turnstile_page(driver):
source = driver.get_page_source()
if (
- "//challenges.cloudflare.com" not in source
- and 'aria-label="Cloudflare"' not in source
+ "//challenges.cloudflare.com" in source
+ or 'aria-label="Cloudflare"' in source
):
+ return True
+ return False
+
+
+def uc_gui_click_cf(driver, frame="iframe", retry=False, blind=False):
+ if not on_a_cf_turnstile_page(driver):
+ return
+ install_pyautogui_if_missing(driver)
+ import pyautogui
+ pyautogui = get_configured_pyautogui(pyautogui)
+ x = None
+ y = None
+ gui_lock = fasteners.InterProcessLock(
+ constants.MultiBrowser.PYAUTOGUILOCK
+ )
+ with gui_lock: # Prevent issues with multiple processes
+ needs_switch = False
+ is_in_frame = js_utils.is_in_frame(driver)
+ if is_in_frame and driver.is_element_present("#challenge-stage"):
+ driver.switch_to.parent_frame()
+ needs_switch = True
+ is_in_frame = js_utils.is_in_frame(driver)
+ if not is_in_frame:
+ # Make sure the window is on top
+ page_actions.switch_to_window(
+ driver, driver.current_window_handle, 2, uc_lock=False
+ )
+ if not is_in_frame or needs_switch:
+ # Currently not in frame (or nested frame outside CF one)
+ try:
+ i_x, i_y = get_gui_element_position(driver, "iframe")
+ driver.switch_to_frame(frame)
+ except Exception:
+ if driver.is_element_present("iframe"):
+ i_x, i_y = get_gui_element_position(driver, "iframe")
+ driver.switch_to_frame("iframe")
+ else:
+ return
+ try:
+ selector = "span"
+ element = driver.wait_for_element_present(selector, timeout=2.5)
+ x = i_x + element.rect["x"] + int(element.rect["width"] / 2) + 1
+ y = i_y + element.rect["y"] + int(element.rect["height"] / 2) + 1
+ driver.switch_to.default_content()
+ except Exception:
+ try:
+ driver.switch_to.default_content()
+ except Exception:
+ return
+ driver.disconnect()
+ try:
+ if x and y:
+ sb_config._saved_cf_x_y = (x, y)
+ uc_gui_click_x_y(driver, x, y, timeframe=0.842, uc_lock=False)
+ except Exception:
+ pass
+ reconnect_time = (float(constants.UC.RECONNECT_TIME) / 2.0) + 0.5
+ if IS_LINUX:
+ reconnect_time = constants.UC.RECONNECT_TIME
+ if not x or not y:
+ reconnect_time = 1 # Make it quick (it already failed)
+ driver.reconnect(reconnect_time)
+ if blind:
+ retry = True
+ if retry and x and y and on_a_cf_turnstile_page(driver):
+ with gui_lock: # Prevent issues with multiple processes
+ # Make sure the window is on top
+ page_actions.switch_to_window(
+ driver, driver.current_window_handle, 2, uc_lock=False
+ )
+ driver.switch_to_frame("iframe")
+ if driver.is_element_visible("#success-icon"):
+ driver.switch_to.parent_frame()
+ return
+ if blind:
+ driver.uc_open_with_disconnect(driver.current_url, 3.8)
+ uc_gui_click_x_y(driver, x, y, timeframe=1.05, uc_lock=False)
+ else:
+ driver.uc_open_with_reconnect(driver.current_url, 3.8)
+ if on_a_cf_turnstile_page(driver):
+ driver.disconnect()
+ uc_gui_click_x_y(
+ driver, x, y, timeframe=1.05, uc_lock=False
+ )
+ driver.reconnect(reconnect_time)
+
+
+def uc_gui_handle_cf(driver, frame="iframe"):
+ if not on_a_cf_turnstile_page(driver):
return
install_pyautogui_if_missing(driver)
import pyautogui
@@ -696,6 +828,8 @@ def uc_gui_handle_cf(driver, frame="iframe"):
except Exception:
pass
reconnect_time = (float(constants.UC.RECONNECT_TIME) / 2.0) + 0.5
+ if IS_LINUX:
+ reconnect_time = constants.UC.RECONNECT_TIME
driver.reconnect(reconnect_time)
@@ -4027,6 +4161,16 @@ def get_local_driver(
driver, *args, **kwargs
)
)
+ driver.uc_gui_click_x_y = (
+ lambda *args, **kwargs: uc_gui_click_x_y(
+ driver, *args, **kwargs
+ )
+ )
+ driver.uc_gui_click_cf = (
+ lambda *args, **kwargs: uc_gui_click_cf(
+ driver, *args, **kwargs
+ )
+ )
driver.uc_gui_handle_cf = (
lambda *args, **kwargs: uc_gui_handle_cf(
driver, *args, **kwargs
diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py
index 1af4806f8a5..1ebae913109 100644
--- a/seleniumbase/fixtures/base_case.py
+++ b/seleniumbase/fixtures/base_case.py
@@ -3393,23 +3393,85 @@ def safe_execute_script(self, script, *args, **kwargs):
self.activate_jquery()
return self.driver.execute_script(script, *args, **kwargs)
+ def get_gui_element_rect(self, selector, by="css selector"):
+ """Very similar to element.rect, but the x, y coordinates are
+ relative to the entire screen, rather than the browser window.
+ This is specifically for PyAutoGUI actions on the full screen.
+ (Note: There may be complications if iframes are involved.)"""
+ element = self.wait_for_element_present(selector, by=by, timeout=1)
+ element_rect = element.rect
+ e_width = element_rect["width"]
+ e_height = element_rect["height"]
+ i_x = 0
+ i_y = 0
+ iframe_switch = False
+ if self.__is_in_frame():
+ self.switch_to_parent_frame()
+ if self.__is_in_frame():
+ raise Exception("Nested iframes breaks get_gui_element_rect!")
+ iframe_switch = True
+ iframe = self.wait_for_element_present("iframe", timeout=1)
+ i_x = iframe.rect["x"]
+ i_y = iframe.rect["y"]
+ window_rect = self.get_window_rect()
+ w_bottom_y = window_rect["y"] + window_rect["height"]
+ viewport_height = self.execute_script("return window.innerHeight;")
+ x = math.ceil(window_rect["x"] + i_x + element_rect["x"])
+ y = math.ceil(w_bottom_y - viewport_height + i_y + element_rect["y"])
+ if iframe_switch:
+ self.switch_to_frame()
+ if not self.is_element_present(selector, by=by):
+ self.switch_to_parent_frame()
+ return ({"height": e_height, "width": e_width, "x": x, "y": y})
+
+ def get_gui_element_center(self, selector, by="css selector"):
+ """Returns the x, y coordinates of the element's center based
+ on the entire GUI / screen, rather than on the browser window.
+ This is specifically for PyAutoGUI actions on the full screen.
+ (Note: There may be complications if iframes are involved.)"""
+ element_rect = self.get_gui_element_rect(selector, by=by)
+ x = int(element_rect["x"]) + int(element_rect["width"] / 2) + 1
+ y = int(element_rect["y"]) + int(element_rect["height"] / 2) + 1
+ return (x, y)
+
+ def get_window_rect(self):
+ self.__check_scope()
+ self._check_browser()
+ return self.driver.get_window_rect()
+
+ def get_window_size(self):
+ self.__check_scope()
+ self._check_browser()
+ return self.driver.get_window_size()
+
+ def get_window_position(self):
+ self.__check_scope()
+ self._check_browser()
+ return self.driver.get_window_position()
+
def set_window_rect(self, x, y, width, height):
self.__check_scope()
self._check_browser()
self.driver.set_window_rect(x, y, width, height)
- self.__demo_mode_pause_if_active()
+ self.__demo_mode_pause_if_active(tiny=True)
def set_window_size(self, width, height):
self.__check_scope()
self._check_browser()
self.driver.set_window_size(width, height)
- self.__demo_mode_pause_if_active()
+ self.__demo_mode_pause_if_active(tiny=True)
+
+ def set_window_position(self, x, y):
+ self.__check_scope()
+ self._check_browser()
+ self.driver.set_window_position(x, y)
+ self.__demo_mode_pause_if_active(tiny=True)
def maximize_window(self):
self.__check_scope()
self._check_browser()
self.driver.maximize_window()
- self.__demo_mode_pause_if_active()
+ self.__demo_mode_pause_if_active(tiny=True)
def switch_to_frame(self, frame="iframe", timeout=None):
"""Wait for an iframe to appear, and switch to it. This should be
@@ -4178,6 +4240,10 @@ def get_new_driver(
self.uc_gui_press_keys = new_driver.uc_gui_press_keys
if hasattr(new_driver, "uc_gui_write"):
self.uc_gui_write = new_driver.uc_gui_write
+ if hasattr(new_driver, "uc_gui_click_x_y"):
+ self.uc_gui_click_x_y = new_driver.uc_gui_click_x_y
+ if hasattr(new_driver, "uc_gui_click_cf"):
+ self.uc_gui_click_cf = new_driver.uc_gui_click_cf
if hasattr(new_driver, "uc_gui_handle_cf"):
self.uc_gui_handle_cf = new_driver.uc_gui_handle_cf
if hasattr(new_driver, "uc_switch_to_frame"):
diff --git a/setup.py b/setup.py
index 5f59c7b9976..df3621f2315 100755
--- a/setup.py
+++ b/setup.py
@@ -151,11 +151,11 @@
'packaging>=24.0;python_version<"3.8"',
'packaging>=24.1;python_version>="3.8"',
'setuptools>=68.0.0;python_version<"3.8"',
- 'setuptools>=70.1.1;python_version>="3.8"',
+ 'setuptools>=70.2.0;python_version>="3.8"',
'wheel>=0.42.0;python_version<"3.8"',
'wheel>=0.43.0;python_version>="3.8"',
'attrs>=23.2.0',
- "certifi>=2024.6.2",
+ "certifi>=2024.7.4",
"exceptiongroup>=1.2.1",
'filelock>=3.12.2;python_version<"3.8"',
'filelock>=3.15.4;python_version>="3.8"',
@@ -177,7 +177,7 @@
'h11==0.14.0',
'outcome==1.3.0.post0',
'trio==0.22.2;python_version<"3.8"',
- 'trio==0.25.1;python_version>="3.8"',
+ 'trio==0.26.0;python_version>="3.8"',
'trio-websocket==0.11.1',
'wsproto==1.2.0',
'websocket-client==1.8.0;python_version>="3.8"',
@@ -270,7 +270,7 @@
# (An optional library for image-processing.)
"pillow": [
'Pillow==9.5.0;python_version<"3.8"',
- 'Pillow>=10.3.0;python_version>="3.8"',
+ 'Pillow>=10.4.0;python_version>="3.8"',
],
# pip install -e .[pip-system-certs]
# (If you see [SSL: CERTIFICATE_VERIFY_FAILED], then get this.)
@@ -282,7 +282,7 @@
# Usage: proxy
# (That starts a proxy server on "127.0.0.1:8899".)
"proxy": [
- "proxy.py==2.4.4",
+ "proxy.py==2.4.3", # 2.4.4 did not have "Listening on ..."
],
# pip install -e .[psutil]
"psutil": [