diff --git a/.gitignore b/.gitignore index 860eff42..46152338 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,4 @@ dist codegen.log Brewfile.lock.json screenshot.png -openapi.v1.yaml +**/.DS_Store diff --git a/examples/e2e/test_playwright.py b/examples/e2e/test_playwright.py new file mode 100644 index 00000000..725d47b1 --- /dev/null +++ b/examples/e2e/test_playwright.py @@ -0,0 +1,78 @@ +import os +from typing import Generator + +import pytest +from dotenv import load_dotenv +from playwright.sync_api import Playwright, sync_playwright + +from browserbase import Browserbase + +from .. import ( + BROWSERBASE_API_KEY, + playwright_basic, + playwright_proxy, + playwright_upload, + playwright_captcha, + playwright_contexts, + playwright_downloads, +) + +bb = Browserbase(api_key=BROWSERBASE_API_KEY) +load_dotenv() + +CI = os.getenv("CI", "false").lower() == "true" + + +@pytest.fixture(scope="session") +def playwright() -> Generator[Playwright, None, None]: + with sync_playwright() as p: + yield p + + +def test_playwright_basic(playwright: Playwright) -> None: + playwright_basic.run(playwright) + + +@pytest.mark.skipif(True, reason="Flaky and fails often") +def test_playwright_captcha(playwright: Playwright) -> None: + playwright_captcha.run(playwright) + + +def test_playwright_contexts(playwright: Playwright) -> None: + playwright_contexts.run(playwright) + + +def test_playwright_downloads(playwright: Playwright) -> None: + playwright_downloads.run(playwright) + + +def test_playwright_proxy_enable_via_create_session(playwright: Playwright) -> None: + playwright_proxy.run_enable_via_create_session(playwright) + + +def test_playwright_proxy_enable_via_querystring(playwright: Playwright) -> None: + playwright_proxy.run_enable_via_querystring_with_created_session(playwright) + + +@pytest.mark.skipif(CI, reason="Flaky and fails on CI") +def test_playwright_proxy_geolocation_country(playwright: Playwright) -> None: + playwright_proxy.run_geolocation_country(playwright) + + +@pytest.mark.skipif(CI, reason="Flaky and fails on CI") +def test_playwright_proxy_geolocation_state(playwright: Playwright) -> None: + playwright_proxy.run_geolocation_state(playwright) + + +@pytest.mark.skipif(CI, reason="Flaky and fails on CI") +def test_playwright_proxy_geolocation_american_city(playwright: Playwright) -> None: + playwright_proxy.run_geolocation_american_city(playwright) + + +@pytest.mark.skipif(CI, reason="Flaky and fails on CI") +def test_playwright_proxy_geolocation_non_american_city(playwright: Playwright) -> None: + playwright_proxy.run_geolocation_non_american_city(playwright) + + +def test_playwright_upload(playwright: Playwright) -> None: + playwright_upload.run(playwright) diff --git a/examples/e2e/test_playwright_basic.py b/examples/e2e/test_playwright_basic.py deleted file mode 100644 index aba94d4c..00000000 --- a/examples/e2e/test_playwright_basic.py +++ /dev/null @@ -1,21 +0,0 @@ -import pytest -from playwright.sync_api import Playwright, sync_playwright - -from browserbase import Browserbase - -from .. import ( - BROWSERBASE_API_KEY, - playwright_basic, -) - -bb = Browserbase(api_key=BROWSERBASE_API_KEY) - - -@pytest.fixture(scope="session") -def playwright(): - with sync_playwright() as p: - yield p - - -def test_playwright_basic(playwright: Playwright): - playwright_basic.run(playwright) diff --git a/examples/packages/extensions/.gitignore b/examples/packages/extensions/.gitignore new file mode 100644 index 00000000..6f66c74b --- /dev/null +++ b/examples/packages/extensions/.gitignore @@ -0,0 +1 @@ +*.zip \ No newline at end of file diff --git a/examples/packages/extensions/browserbase-test/hello.html b/examples/packages/extensions/browserbase-test/hello.html new file mode 100644 index 00000000..2a0b7102 --- /dev/null +++ b/examples/packages/extensions/browserbase-test/hello.html @@ -0,0 +1,5 @@ + + +

Hello Extensions

+ + diff --git a/examples/packages/extensions/browserbase-test/images/logo.png b/examples/packages/extensions/browserbase-test/images/logo.png new file mode 100644 index 00000000..583360a1 Binary files /dev/null and b/examples/packages/extensions/browserbase-test/images/logo.png differ diff --git a/examples/packages/extensions/browserbase-test/manifest.json b/examples/packages/extensions/browserbase-test/manifest.json new file mode 100644 index 00000000..a3d41e7c --- /dev/null +++ b/examples/packages/extensions/browserbase-test/manifest.json @@ -0,0 +1,21 @@ +{ + "manifest_version": 3, + "name": "Browserbase Extension Test", + "description": "Test extension for browserbase", + "version": "1.0", + "action": { + "default_popup": "hello.html" + }, + "content_scripts": [ + { + "matches": ["https://www.browserbase.com/*"], + "js": ["scripts/content.js"] + } + ], + "web_accessible_resources": [ + { + "resources": ["images/logo.png"], + "matches": ["https://www.browserbase.com/*"] + } + ] +} diff --git a/examples/packages/extensions/browserbase-test/scripts/content.js b/examples/packages/extensions/browserbase-test/scripts/content.js new file mode 100644 index 00000000..abd195a7 --- /dev/null +++ b/examples/packages/extensions/browserbase-test/scripts/content.js @@ -0,0 +1,11 @@ +const imageUrl = chrome.runtime.getURL("images/logo.png"); +window + .fetch(imageUrl) + .then((response) => { + if (response.ok) { + console.log("browserbase test extension image loaded"); + } + }) + .catch((error) => { + console.log(error); + }); diff --git a/examples/packages/logo.png b/examples/packages/logo.png new file mode 100644 index 00000000..583360a1 Binary files /dev/null and b/examples/packages/logo.png differ diff --git a/examples/playwright_basic.py b/examples/playwright_basic.py index e909f9db..ac059171 100644 --- a/examples/playwright_basic.py +++ b/examples/playwright_basic.py @@ -1,25 +1,20 @@ from playwright.sync_api import Playwright, sync_playwright from examples import ( - BROWSERBASE_API_KEY, BROWSERBASE_PROJECT_ID, - BROWSERBASE_CONNECT_URL, bb, ) -def run(playwright: Playwright): +def run(playwright: Playwright) -> None: # Create a session on Browserbase session = bb.sessions.create(project_id=BROWSERBASE_PROJECT_ID) assert session.id is not None assert session.status == "RUNNING", f"Session status is {session.status}" # Connect to the remote session - connect_url = ( - f"{BROWSERBASE_CONNECT_URL}?sessionId={session.id}&apiKey={BROWSERBASE_API_KEY}" - ) chromium = playwright.chromium - browser = chromium.connect_over_cdp(connect_url) + browser = chromium.connect_over_cdp(session.connect_url) context = browser.contexts[0] page = context.pages[0] diff --git a/examples/playwright_captcha.py b/examples/playwright_captcha.py new file mode 100644 index 00000000..a5f0b431 --- /dev/null +++ b/examples/playwright_captcha.py @@ -0,0 +1,60 @@ +from playwright.sync_api import Playwright, ConsoleMessage, sync_playwright + +from examples import ( + BROWSERBASE_API_KEY, + BROWSERBASE_PROJECT_ID, + BROWSERBASE_CONNECT_URL, + bb, +) + +DEFAULT_CAPTCHA_URL = "https://www.google.com/recaptcha/api2/demo" +OVERRIDE_TIMEOUT = 60000 # 60 seconds, adjust as needed + + +def run(playwright: Playwright) -> None: + # Create a session on Browserbase + session = bb.sessions.create(project_id=BROWSERBASE_PROJECT_ID) + assert session.id is not None + assert session.status == "RUNNING", f"Session status is {session.status}" + + # Connect to the remote session + connect_url = ( + f"{BROWSERBASE_CONNECT_URL}?sessionId={session.id}&apiKey={BROWSERBASE_API_KEY}" + ) + chromium = playwright.chromium + browser = chromium.connect_over_cdp(connect_url) + context = browser.contexts[0] + page = context.pages[0] + + captcha_solving_started = False + captcha_solving_finished = False + + # Browserbase logs messages to the console to indicate when captcha solving has started and finished + # We can track these messages to know when the captcha solving has started and finished + def handle_console(msg: ConsoleMessage) -> None: + nonlocal captcha_solving_started, captcha_solving_finished + if msg.text == "browserbase-solving-started": + captcha_solving_started = True + page.evaluate("window.captchaSolvingFinished = false;") + elif msg.text == "browserbase-solving-finished": + captcha_solving_finished = True + page.evaluate("window.captchaSolvingFinished = true;") + + page.on("console", handle_console) + + page.goto(DEFAULT_CAPTCHA_URL, wait_until="networkidle") + page.wait_for_function( + "() => window.captchaSolvingFinished === true", timeout=OVERRIDE_TIMEOUT + ) + + assert captcha_solving_started, "Captcha solving did not start" + assert captcha_solving_finished, "Captcha solving did not finish" + + page.close() + browser.close() + print("Done!") + + +if __name__ == "__main__": + with sync_playwright() as playwright: + run(playwright) diff --git a/examples/playwright_contexts.py b/examples/playwright_contexts.py new file mode 100644 index 00000000..6776a088 --- /dev/null +++ b/examples/playwright_contexts.py @@ -0,0 +1,128 @@ +import time +from typing import Optional + +from playwright.sync_api import Cookie, Browser, Playwright, sync_playwright + +from examples import ( + BROWSERBASE_API_KEY, + BROWSERBASE_PROJECT_ID, + BROWSERBASE_CONNECT_URL, + bb, +) +from browserbase.types.session_create_params import ( + BrowserSettings, + BrowserSettingsContext, +) + +CONTEXT_TEST_URL = "https://www.browserbase.com" +SECOND = 1000 + + +def add_hour(date: float) -> int: + return int((date + 3600) * 1000) // SECOND + + +def find_cookie(browser: Browser, name: str) -> Optional[Cookie]: + default_context = browser.contexts[0] + cookies = default_context.cookies() + return next((cookie for cookie in cookies if cookie.get("name") == name), None) + + +def run(playwright: Playwright) -> None: + context_id = None + session_id = None + test_cookie_name = None + test_cookie_value = None + + # Step 1: Creates a context + context = bb.contexts.create(project_id=BROWSERBASE_PROJECT_ID) + assert context.id is not None + context_id = context.id + + uploaded_context = bb.contexts.retrieve(id=context_id) + assert uploaded_context.id == context_id + + # Step 2: Creates a session with the context + session = bb.sessions.create( + project_id=BROWSERBASE_PROJECT_ID, + browser_settings=BrowserSettings( + context=BrowserSettingsContext(id=context_id, persist=True), + ), + ) + + assert ( + session.context_id == context_id + ), f"Session context_id is {session.context_id}, expected {context_id}" + session_id = session.id + + # Step 3: Populates and persists the context + print(f"Populating context {context_id} during session {session_id}") + connect_url = ( + f"{BROWSERBASE_CONNECT_URL}?sessionId={session_id}&apiKey={BROWSERBASE_API_KEY}" + ) + browser = playwright.chromium.connect_over_cdp(connect_url) + page = browser.contexts[0].pages[0] + + page.goto(CONTEXT_TEST_URL, wait_until="domcontentloaded") + + now = time.time() + test_cookie_name = f"bb_{int(now * 1000)}" + test_cookie_value = time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime(now)) + browser.contexts[0].add_cookies( + [ + { + "domain": ".browserbase.com", + "expires": add_hour(now), + "name": test_cookie_name, + "path": "/", + "value": test_cookie_value, + } + ] + ) + + assert find_cookie(browser, test_cookie_name) is not None + + page.goto("https://www.google.com", wait_until="domcontentloaded") + page.go_back() + + assert find_cookie(browser, test_cookie_name) is not None + + page.close() + browser.close() + + time.sleep(5) + + # Step 4: Creates another session with the same context + session = bb.sessions.create( + project_id=BROWSERBASE_PROJECT_ID, + browser_settings=BrowserSettings( + context=BrowserSettingsContext(id=context_id, persist=True) + ), + ) + assert ( + session.context_id == context_id + ), f"Session context_id is {session.context_id}, expected {context_id}" + session_id = session.id + + # Step 5: Uses context to find previous state + print(f"Reusing context {context_id} during session {session_id}") + connect_url = ( + f"{BROWSERBASE_CONNECT_URL}?sessionId={session_id}&apiKey={BROWSERBASE_API_KEY}" + ) + browser = playwright.chromium.connect_over_cdp(connect_url) + page = browser.contexts[0].pages[0] + + page.goto(CONTEXT_TEST_URL, wait_until="domcontentloaded") + + found_cookie = find_cookie(browser, test_cookie_name) + print(found_cookie) + assert found_cookie is not None + assert found_cookie.get("value") == test_cookie_value + + page.close() + browser.close() + + +if __name__ == "__main__": + with sync_playwright() as playwright: + run(playwright) diff --git a/examples/playwright_downloads.py b/examples/playwright_downloads.py new file mode 100644 index 00000000..bded0cff --- /dev/null +++ b/examples/playwright_downloads.py @@ -0,0 +1,88 @@ +import io +import re +import zipfile + +from playwright.sync_api import Playwright, sync_playwright + +from examples import ( + BROWSERBASE_API_KEY, + BROWSERBASE_PROJECT_ID, + BROWSERBASE_CONNECT_URL, + bb, +) + +download_re = re.compile(r"sandstorm-(\d{13})+\.mp3") + + +def get_download(session_id: str) -> bytes: + response = bb.sessions.downloads.list(id=session_id) + return response.read() + + +def run(playwright: Playwright) -> None: + # Create a session on Browserbase + session = bb.sessions.create(project_id=BROWSERBASE_PROJECT_ID) + assert session.id is not None + assert session.status == "RUNNING", f"Session status is {session.status}" + + # Connect to the remote session + connect_url = ( + f"{BROWSERBASE_CONNECT_URL}?sessionId={session.id}&apiKey={BROWSERBASE_API_KEY}" + ) + browser = playwright.chromium.connect_over_cdp(connect_url) + context = browser.contexts[0] + page = context.pages[0] + + # Set up CDP session for download behavior + client = context.new_cdp_session(page) + client.send( # pyright: ignore + "Browser.setDownloadBehavior", + { + "behavior": "allow", + "downloadPath": "downloads", + "eventsEnabled": True, + }, + ) + + # Navigate to the download test page + page.goto("https://browser-tests-alpha.vercel.app/api/download-test") + + # Start download and wait for it to complete + with page.expect_download() as download_info: + page.locator("#download").click() + download = download_info.value + + # Check for download errors + download_error = download.failure() + if download_error: + raise Exception(f"Download for session {session.id} failed: {download_error}") + + page.close() + browser.close() + + # Verify the download + zip_buffer = get_download(session.id) + if len(zip_buffer) == 0: + raise Exception(f"Download buffer is empty for session {session.id}") + + zip_file = zipfile.ZipFile(io.BytesIO(zip_buffer)) + zip_entries = zip_file.namelist() + mp3_entry = next((entry for entry in zip_entries if download_re.match(entry)), None) + + if not mp3_entry: + raise Exception( + f"Session {session.id} is missing a file matching '{download_re.pattern}' in its zip entries: {zip_entries}" + ) + + expected_file_size = 6137541 + actual_file_size = zip_file.getinfo(mp3_entry).file_size + assert ( + actual_file_size == expected_file_size + ), f"Expected file size {expected_file_size}, but got {actual_file_size}" + + print("Download test passed successfully!") + + +if __name__ == "__main__": + with sync_playwright() as playwright: + run(playwright) diff --git a/examples/playwright_extensions.py b/examples/playwright_extensions.py new file mode 100644 index 00000000..d12f47a0 --- /dev/null +++ b/examples/playwright_extensions.py @@ -0,0 +1,159 @@ +import os +import time +import zipfile +from io import BytesIO +from pathlib import Path + +from playwright.sync_api import Page, Playwright, sync_playwright + +from examples import ( + BROWSERBASE_API_KEY, + BROWSERBASE_PROJECT_ID, + BROWSERBASE_CONNECT_URL, + bb, +) +from browserbase.types import Extension, SessionCreateResponse + +PATH_TO_EXTENSION = ( + Path.cwd() / "examples" / "packages" / "extensions" / "browserbase-test" +) + + +def zip_extension(path: Path = PATH_TO_EXTENSION, save_local: bool = False) -> BytesIO: + """ + Create an in-memory zip file from the contents of the given folder. + Mark save_local=True to save the zip file to a local file. + """ + # Ensure we're looking at an extension + assert "manifest.json" in os.listdir( + path + ), "No manifest.json found in the extension folder." + + # Create a BytesIO object to hold the zip file in memory + memory_zip = BytesIO() + + # Create a ZipFile object + with zipfile.ZipFile(memory_zip, "w", zipfile.ZIP_DEFLATED) as zf: + # Recursively walk through the directory + for root, _, files in os.walk(path): + for file in files: + # Create the full file path + file_path = os.path.join(root, file) + # Calculate the archive name (path relative to the root directory) + archive_name = os.path.relpath(file_path, path) + # Add the file to the zip + zf.write(file_path, archive_name) + + if save_local: + with open(f"{path}.zip", "wb") as f: + f.write(memory_zip.getvalue()) + + return memory_zip + + +def create_extension() -> str: + zip_data = zip_extension(save_local=True) + extension: Extension = bb.extensions.create( + file=("extension.zip", zip_data.getvalue()) + ) + return extension.id + + +def get_extension(id: str) -> Extension: + return bb.extensions.retrieve(id) + + +def delete_extension(id: str) -> None: + bb.extensions.delete(id) + + +def check_for_message(page: Page, message: str) -> None: + # Wait for the extension to load and log a message + console_messages: list[str] = [] + page.on("console", lambda msg: console_messages.append(msg.text)) + page.goto("https://www.browserbase.com/") + + start = time.time() + while time.time() - start < 10: + if message in console_messages: + break + assert ( + message in console_messages + ), f"Expected message not found in console logs. Messages: {console_messages}" + + +def run(playwright: Playwright) -> None: + expected_message = "browserbase test extension image loaded" + extension_id = None + + # Create extension + extension_id = create_extension() + print(f"Created extension with ID: {extension_id}") + + # Get extension + extension = get_extension(extension_id) + print(f"Retrieved extension: {extension}") + + # Use extension + session: SessionCreateResponse = bb.sessions.create( + project_id=BROWSERBASE_PROJECT_ID, + extension_id=extension.id, + ) + + browser = playwright.chromium.connect_over_cdp( + f"{BROWSERBASE_CONNECT_URL}?apiKey={BROWSERBASE_API_KEY}&sessionId={session.id}" + ) + context = browser.contexts[0] + page = context.pages[0] + check_for_message(page, expected_message) + page.close() + browser.close() + + # Use extension with proxies + session_with_proxy: SessionCreateResponse = bb.sessions.create( + project_id=BROWSERBASE_PROJECT_ID, + extension_id=extension_id, + proxies=True, + ) + + browser = playwright.chromium.connect_over_cdp( + f"{BROWSERBASE_CONNECT_URL}?apiKey={BROWSERBASE_API_KEY}&sessionId={session_with_proxy.id}" + ) + context = browser.contexts[0] + page = context.pages[0] + + console_messages: list[str] = [] + page.on("console", lambda msg: console_messages.append(msg.text)) + + page.goto("https://www.browserbase.com/") + + check_for_message(page, expected_message) + page.close() + browser.close() + + # Delete extension + delete_extension(extension_id) + print(f"Deleted extension with ID: {extension_id}") + + # Verify deleted extension is unusable + try: + get_extension(extension_id) + raise AssertionError("Expected to fail when retrieving deleted extension") + except Exception as e: + print(f"Failed to get deleted extension as expected: {str(e)}") + + try: + bb.sessions.create( + project_id=BROWSERBASE_PROJECT_ID, + extension_id=extension_id, + ) + raise AssertionError( + "Expected to fail when creating session with deleted extension" + ) + except Exception as e: + print(f"Failed to create session with deleted extension as expected: {str(e)}") + + +if __name__ == "__main__": + with sync_playwright() as playwright: + run(playwright) diff --git a/examples/playwright_proxy.py b/examples/playwright_proxy.py new file mode 100644 index 00000000..4ea6677b --- /dev/null +++ b/examples/playwright_proxy.py @@ -0,0 +1,198 @@ +import time + +from playwright.sync_api import Page, Playwright, sync_playwright + +from examples import ( + BROWSERBASE_API_KEY, + BROWSERBASE_PROJECT_ID, + BROWSERBASE_CONNECT_URL, + bb, +) + +GRACEFUL_SHUTDOWN_TIMEOUT = 30000 # Assuming 30 seconds, adjust as needed + + +def check_proxy_bytes(session_id: str) -> None: + bb.sessions.update( + id=session_id, project_id=BROWSERBASE_PROJECT_ID, status="REQUEST_RELEASE" + ) + time.sleep(GRACEFUL_SHUTDOWN_TIMEOUT / 1000) + updated_session = bb.sessions.retrieve(id=session_id) + assert ( + updated_session.proxy_bytes is not None and updated_session.proxy_bytes > 0 + ), f"Proxy bytes: {updated_session.proxy_bytes}" + + +def run_enable_via_create_session(playwright: Playwright) -> None: + session = bb.sessions.create(project_id=BROWSERBASE_PROJECT_ID, proxies=True) + + browser = playwright.chromium.connect_over_cdp( + f"{BROWSERBASE_CONNECT_URL}?apiKey={BROWSERBASE_API_KEY}&sessionId={session.id}" + ) + + context = browser.contexts[0] + page = context.pages[0] + page.goto("https://www.google.com") + page_title = page.title() + + page.close() + browser.close() + + assert page_title == "Google" + check_proxy_bytes(session.id) + + +def run_enable_via_querystring_with_created_session(playwright: Playwright) -> None: + session = bb.sessions.create(project_id=BROWSERBASE_PROJECT_ID, proxies=True) + + browser = playwright.chromium.connect_over_cdp( + f"{BROWSERBASE_CONNECT_URL}?apiKey={BROWSERBASE_API_KEY}&sessionId={session.id}&enableProxy=true" + ) + + context = browser.contexts[0] + page = context.pages[0] + page.goto("https://www.google.com/") + page_title = page.title() + + page.close() + browser.close() + + assert page_title == "Google" + check_proxy_bytes(session.id) + + +def extract_from_table(page: Page, cell: str) -> str: + page.goto("https://www.showmyip.com/") + page.wait_for_selector("table.iptab") + + td = page.locator(f"table.iptab tr:has-text('{cell}') td:last-child") + + text = td.text_content() + if not text: + raise Exception(f"Failed to extract {cell}") + return text.strip() + + +def run_geolocation_country(playwright: Playwright) -> None: + session = bb.sessions.create( + project_id=BROWSERBASE_PROJECT_ID, + proxies=[ + { + "geolocation": {"country": "CA"}, + "type": "browserbase", + } + ], + ) + + browser = playwright.chromium.connect_over_cdp( + f"{BROWSERBASE_CONNECT_URL}?apiKey={BROWSERBASE_API_KEY}&sessionId={session.id}" + ) + + context = browser.contexts[0] + page = context.pages[0] + + country = extract_from_table(page, "Country") + + page.close() + browser.close() + + assert country == "Canada" + + +def run_geolocation_state(playwright: Playwright) -> None: + session = bb.sessions.create( + project_id=BROWSERBASE_PROJECT_ID, + proxies=[ + { + "geolocation": { + "country": "US", + "state": "NY", + }, + "type": "browserbase", + } + ], + ) + + browser = playwright.chromium.connect_over_cdp( + f"{BROWSERBASE_CONNECT_URL}?apiKey={BROWSERBASE_API_KEY}&sessionId={session.id}" + ) + + context = browser.contexts[0] + page = context.pages[0] + + state = extract_from_table(page, "Region") + + page.close() + browser.close() + + assert state == "New York" + + +def run_geolocation_american_city(playwright: Playwright) -> None: + session = bb.sessions.create( + project_id=BROWSERBASE_PROJECT_ID, + proxies=[ + { + "geolocation": { + "city": "Los Angeles", + "country": "US", + "state": "CA", + }, + "type": "browserbase", + } + ], + ) + + browser = playwright.chromium.connect_over_cdp( + f"{BROWSERBASE_CONNECT_URL}?apiKey={BROWSERBASE_API_KEY}&sessionId={session.id}" + ) + + context = browser.contexts[0] + page = context.pages[0] + + city = extract_from_table(page, "City") + + page.close() + browser.close() + + assert city == "Los Angeles" + + +def run_geolocation_non_american_city(playwright: Playwright) -> None: + session = bb.sessions.create( + project_id=BROWSERBASE_PROJECT_ID, + proxies=[ + { + "geolocation": { + "city": "London", + "country": "GB", + }, + "type": "browserbase", + } + ], + ) + + browser = playwright.chromium.connect_over_cdp( + f"{BROWSERBASE_CONNECT_URL}?apiKey={BROWSERBASE_API_KEY}&sessionId={session.id}" + ) + + context = browser.contexts[0] + page = context.pages[0] + + city = extract_from_table(page, "City") + + page.close() + browser.close() + + assert city == "London" + + +if __name__ == "__main__": + with sync_playwright() as playwright: + # You can run any of these tests by uncommenting them + run_enable_via_create_session(playwright) + # run_enable_via_querystring_with_created_session(playwright) + # run_geolocation_country(playwright) + # run_geolocation_state(playwright) + # run_geolocation_american_city(playwright) + # run_geolocation_non_american_city(playwright) diff --git a/examples/playwright_upload.py b/examples/playwright_upload.py new file mode 100644 index 00000000..c1a2237c --- /dev/null +++ b/examples/playwright_upload.py @@ -0,0 +1,63 @@ +from pathlib import Path + +from playwright.sync_api import Playwright, sync_playwright + +from examples import ( + BROWSERBASE_API_KEY, + BROWSERBASE_PROJECT_ID, + BROWSERBASE_CONNECT_URL, + bb, +) + +PATH_TO_UPLOAD = Path.cwd() / "examples" / "packages" / "logo.png" + + +def run(playwright: Playwright) -> None: + # Create a session + session = bb.sessions.create(project_id=BROWSERBASE_PROJECT_ID) + + # Construct the URL + url = ( + f"{BROWSERBASE_CONNECT_URL}?apiKey={BROWSERBASE_API_KEY}&sessionId={session.id}" + ) + + # Connect to the browser + browser = playwright.chromium.connect_over_cdp(url) + context = browser.contexts[0] + page = context.pages[0] + + try: + # Navigate to the upload test page + page.goto("https://browser-tests-alpha.vercel.app/api/upload-test") + + # Locate the file input element + file_input = page.locator("#fileUpload") + file_input.set_input_files(str(PATH_TO_UPLOAD)) + + # Get the uploaded file name + file_name_span = page.locator("#fileName") + file_name = file_name_span.inner_text() + + # Get the uploaded file size + file_size_span = page.locator("#fileSize") + file_size = int(file_size_span.inner_text()) + + # Assert the file name and size + assert ( + file_name == "logo.png" + ), f"Expected file name to be 'logo.png', but got '{file_name}'" + assert ( + file_size > 0 + ), f"Expected file size to be greater than 0, but got {file_size}" + + print("File upload test passed successfully!") + + finally: + # Clean up + page.close() + browser.close() + + +if __name__ == "__main__": + with sync_playwright() as playwright: + run(playwright) diff --git a/openapi.v1.yaml b/openapi.v1.yaml new file mode 100644 index 00000000..7bb296d0 --- /dev/null +++ b/openapi.v1.yaml @@ -0,0 +1,1142 @@ +openapi: 3.0.0 +info: + title: Browserbase API + description: Browserbase API for 3rd party developers + version: v1 +tags: [] +paths: + /v1/contexts: + post: + operationId: Contexts_create + summary: Create a Context + parameters: [] + responses: + "201": + description: The request has succeeded and a new resource has been created as a result. + content: + application/json: + schema: + $ref: "#/components/schemas/CreateContextResponse" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateContext" + /v1/contexts/{id}: + get: + operationId: Contexts_get + summary: Context + parameters: + - name: id + in: path + required: true + schema: + $ref: "#/components/schemas/uuid" + responses: + "200": + description: The request has succeeded. + content: + application/json: + schema: + $ref: "#/components/schemas/Context" + put: + operationId: Contexts_update + summary: Update Context + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: The request has succeeded. + content: + application/json: + schema: + $ref: "#/components/schemas/CreateContextResponse" + /v1/extensions: + post: + operationId: Extensions_upload + summary: Upload an Extension + parameters: [] + responses: + "200": + description: The request has succeeded. + content: + application/json: + schema: + $ref: "#/components/schemas/Extension" + "415": + description: Client error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + required: + - file + /v1/extensions/{id}: + get: + operationId: Extensions_get + summary: Extension + parameters: + - name: id + in: path + required: true + schema: + $ref: "#/components/schemas/uuid" + responses: + "200": + description: The request has succeeded. + content: + application/json: + schema: + $ref: "#/components/schemas/Extension" + delete: + operationId: Extensions_delete + summary: Delete Extension + parameters: + - name: id + in: path + required: true + schema: + $ref: "#/components/schemas/uuid" + responses: + "204": + description: "There is no content to send for this request, but the headers may be useful. " + /v1/projects: + get: + operationId: Projects_list + summary: List all projects + parameters: [] + responses: + "200": + description: The request has succeeded. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Project" + /v1/projects/{id}: + get: + operationId: Projects_get + summary: Project + parameters: + - name: id + in: path + required: true + schema: + $ref: "#/components/schemas/uuid" + responses: + "200": + description: The request has succeeded. + content: + application/json: + schema: + $ref: "#/components/schemas/Project" + /v1/projects/{id}/usage: + get: + operationId: Projects_usage + summary: Project Usage + parameters: + - name: id + in: path + required: true + schema: + $ref: "#/components/schemas/uuid" + responses: + "200": + description: The request has succeeded. + content: + application/json: + schema: + $ref: "#/components/schemas/ProjectUsage" + /v1/sessions: + get: + operationId: Sessions_list + summary: List Sessions + parameters: + - name: status + in: query + required: false + schema: + $ref: "#/components/schemas/SessionStatus" + explode: false + responses: + "200": + description: The request has succeeded. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Session" + post: + operationId: Sessions_create + summary: Create a Session + parameters: [] + responses: + "201": + description: The request has succeeded and a new resource has been created as a result. + content: + application/json: + schema: + type: object + properties: + projectId: + allOf: + - $ref: "#/components/schemas/uuid" + description: The Project ID linked to the Session. + startedAt: + type: string + format: date-time + endedAt: + type: string + format: date-time + expiresAt: + type: string + format: date-time + status: + $ref: "#/components/schemas/SessionStatus" + proxyBytes: + type: integer + description: Bytes used via the [Proxy](/features/stealth-mode#proxies-and-residential-ips) + avgCpuUsage: + type: integer + description: CPU used by the Session + memoryUsage: + type: integer + description: Memory used by the Session + keepAlive: + type: boolean + description: Indicates if the Session was created to be kept alive upon disconnections + contextId: + allOf: + - $ref: "#/components/schemas/uuid" + description: Optional. The Context linked to the Session. + region: + allOf: + - $ref: "#/components/schemas/Region" + description: The region where the Session is running. + id: + $ref: "#/components/schemas/uuid" + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + connectUrl: + type: string + format: uri + description: WebSocket URL to connect to the Session. + seleniumRemoteUrl: + type: string + format: uri + description: HTTP URL to connect to the Session. + signingKey: + type: string + description: Signing key to use when connecting to the Session via HTTP. + required: + - projectId + - startedAt + - expiresAt + - status + - proxyBytes + - keepAlive + - region + - id + - createdAt + - updatedAt + - connectUrl + - seleniumRemoteUrl + - signingKey + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateSession" + x-codeSamples: + - lang: cURL + source: |- + curl --request POST \ + --url https://www.browserbase.com/v1/sessions \ + --header 'Content-Type: application/json' \ + --header 'X-BB-API-Key: ' \ + --data '{"projectId": ""}' + - lang: JavaScript + source: |- + fetch('https://www.browserbase.com/v1/sessions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-BB-API-Key': '' + }, + body: JSON.stringify({ + "projectId": "", + }) + }) + - lang: Python + source: |- + import requests + + url = "https://www.browserbase.com/v1/sessions" + + payload = { + "projectId": "", + } + headers = { + "X-BB-API-Key": "", + "Content-Type": "application/json" + } + + response = requests.request("POST", url, json=payload, headers=headers) + + print(response.text) + - lang: PHP + source: |- + "https://www.browserbase.com/v1/sessions", + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "POST", + CURLOPT_POSTFIELDS => "{"projectId": ""}", + CURLOPT_HTTPHEADER => [ + "Content-Type: application/json", + "X-BB-API-Key: " + ], + ]); + + $response = curl_exec($curl); + $err = curl_error($curl); + + curl_close($curl); + + if ($err) { + echo "cURL Error #:" . $err; + } else { + echo $response; + } + - lang: Go + source: |- + package main + + import ( + "fmt" + "strings" + "net/http" + "io/ioutil" + ) + + func main() { + + url := "https://www.browserbase.com/v1/sessions" + + payload := strings.NewReader("{"projectId": ""}") + + req, _ := http.NewRequest("POST", url, payload) + + req.Header.Add("X-BB-API-Key", "") + req.Header.Add("Content-Type", "application/json") + + res, _ := http.DefaultClient.Do(req) + + defer res.Body.Close() + body, _ := ioutil.ReadAll(res.Body) + + fmt.Println(res) + fmt.Println(string(body)) + + } + - lang: Java + source: |- + HttpResponse response = Unirest.post("https://www.browserbase.com/v1/sessions") + .header("X-BB-API-Key", "") + .header("Content-Type", "application/json") + .body("{"projectId": ""}") + .asString(); + /v1/sessions/{id}: + get: + operationId: Sessions_get + summary: Session + parameters: + - name: id + in: path + required: true + schema: + $ref: "#/components/schemas/uuid" + responses: + "200": + description: The request has succeeded. + content: + application/json: + schema: + $ref: "#/components/schemas/Session" + post: + operationId: Sessions_update + summary: Update Session + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: The request has succeeded. + content: + application/json: + schema: + $ref: "#/components/schemas/Session" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/SessionUpdate" + /v1/sessions/{id}/debug: + get: + operationId: Sessions_getDebug + summary: Session Live URLs + parameters: + - name: id + in: path + required: true + schema: + $ref: "#/components/schemas/uuid" + responses: + "200": + description: The request has succeeded. + content: + application/json: + schema: + $ref: "#/components/schemas/SessionLiveUrls" + /v1/sessions/{id}/downloads: + get: + operationId: Sessions_getDownloads + summary: Session Downloads + parameters: + - name: id + in: path + required: true + schema: + $ref: "#/components/schemas/uuid" + responses: + "200": + description: The request has succeeded. + content: + application/zip: + schema: + type: string + format: binary + /v1/sessions/{id}/logs: + get: + operationId: Sessions_getLogs + summary: Session Logs + parameters: + - name: id + in: path + required: true + schema: + $ref: "#/components/schemas/uuid" + responses: + "200": + description: The request has succeeded. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/SessionLog" + "422": + description: Client error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /v1/sessions/{id}/recording: + get: + operationId: Sessions_getRecording + summary: Session Recording + parameters: + - name: id + in: path + required: true + schema: + $ref: "#/components/schemas/uuid" + responses: + "200": + description: The request has succeeded. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/SessionRecording" + "422": + description: Client error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /v1/sessions/{id}/uploads: + post: + operationId: Sessions_uploadFile + summary: Create Session Uploads + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: The request has succeeded. + content: + application/json: + schema: + type: object + properties: + message: + type: string + required: + - message + "415": + description: Client error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + required: + - file +security: + - BrowserbaseAuth: [] +components: + schemas: + BrowserbaseAuth: + type: object + required: + - type + - in + - name + properties: + type: + type: string + enum: + - apiKey + description: API key authentication + in: + type: string + enum: + - header + description: location of the API key + name: + type: string + enum: + - X-BB-API-Key + description: name of the API key + description: Your [Browserbase API Key](https://www.browserbase.com/settings). + BrowserbaseProxyConfig: + type: object + required: + - type + properties: + type: + type: string + enum: + - browserbase + description: Type of proxy. Always use 'browserbase' for the Browserbase managed proxy network. + geolocation: + allOf: + - $ref: "#/components/schemas/GeolocationConfig" + description: Geographic location for the proxy. Optional. + domainPattern: + type: string + description: Domain pattern for which this proxy should be used. If omitted, defaults to all domains. Optional. + Context: + type: object + required: + - id + - createdAt + - updatedAt + - projectId + properties: + id: + $ref: "#/components/schemas/uuid" + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + projectId: + allOf: + - $ref: "#/components/schemas/uuid" + description: The Project ID linked to the uploaded Context. + ContextSetting: + type: object + required: + - id + properties: + id: + allOf: + - $ref: "#/components/schemas/uuid" + description: The Context ID. + persist: + type: boolean + description: Whether or not to persist the context after browsing. Defaults to `false`. + CreateContext: + type: object + required: + - projectId + properties: + projectId: + allOf: + - $ref: "#/components/schemas/uuid" + description: The Project ID. Can be found in [Settings](https://www.browserbase.com/settings). + CreateContextResponse: + type: object + required: + - id + - uploadUrl + - publicKey + - cipherAlgorithm + - initializationVectorSize + properties: + id: + $ref: "#/components/schemas/uuid" + uploadUrl: + type: string + minLength: 1 + description: An upload URL to upload a custom user-data-directory. + publicKey: + type: string + description: The public key to encrypt the user-data-directory. + cipherAlgorithm: + type: string + description: The cipher algorithm used to encrypt the user-data-directory. AES-256-CBC is currently the only supported algorithm. + initializationVectorSize: + type: integer + format: uint8 + description: The initialization vector size used to encrypt the user-data-directory. [Read more about how to use it](/features/contexts). + CreateSession: + type: object + required: + - projectId + properties: + projectId: + allOf: + - $ref: "#/components/schemas/uuid" + description: The Project ID. Can be found in [Settings](https://www.browserbase.com/settings). + extensionId: + allOf: + - $ref: "#/components/schemas/uuid" + description: The uploaded Extension ID. See [Upload Extension](/reference/api/upload-an-extension). + browserSettings: + $ref: "#/components/schemas/SessionBrowserSettings" + timeout: + type: integer + minimum: 60 + maximum: 21600 + description: Duration in seconds after which the session will automatically end. Defaults to the Project's `defaultTimeout`. + keepAlive: + type: boolean + description: Set to true to keep the session alive even after disconnections. This is available on the Startup plan only. + proxies: + description: Proxy configuration. Can be true for default proxy, or an array of proxy configurations. + region: + allOf: + - $ref: "#/components/schemas/Region" + description: The region where the Session should run. + CreateSessionConnectDetails: + type: object + required: + - connectUrl + - seleniumRemoteUrl + - signingKey + properties: + connectUrl: + type: string + format: uri + description: WebSocket URL to connect to the Session. + seleniumRemoteUrl: + type: string + format: uri + description: HTTP URL to connect to the Session. + signingKey: + type: string + description: Signing key to use when connecting to the Session via HTTP. + Entity: + type: object + required: + - id + - createdAt + - updatedAt + properties: + id: + $ref: "#/components/schemas/uuid" + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + message: + type: string + Extension: + type: object + required: + - id + - createdAt + - updatedAt + - fileName + - projectId + properties: + id: + $ref: "#/components/schemas/uuid" + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + fileName: + type: string + minLength: 1 + projectId: + allOf: + - $ref: "#/components/schemas/uuid" + description: The Project ID linked to the uploaded Extension. + ExternalProxyConfig: + type: object + required: + - type + - server + properties: + type: + type: string + enum: + - external + description: Type of proxy. Always 'external' for this config. + server: + type: string + description: Server URL for external proxy. Required. + domainPattern: + type: string + description: Domain pattern for which this proxy should be used. If omitted, defaults to all domains. Optional. + username: + type: string + description: Username for external proxy authentication. Optional. + password: + type: string + description: Password for external proxy authentication. Optional. + Fingerprint: + type: object + properties: + httpVersion: + type: number + enum: + - 1 + - 2 + browsers: + type: array + items: + type: string + enum: + - chrome + - edge + - firefox + - safari + devices: + type: array + items: + type: string + enum: + - desktop + - mobile + locales: + type: array + items: + type: string + description: Full list of locales is available [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language). + operatingSystems: + type: array + items: + type: string + enum: + - android + - ios + - linux + - macos + - windows + description: 'Note: `operatingSystems` set to `ios` or `android` requires `devices` to include `"mobile"`.' + screen: + $ref: "#/components/schemas/FingerprintScreen" + FingerprintScreen: + type: object + properties: + maxHeight: + type: integer + maxWidth: + type: integer + minHeight: + type: integer + minWidth: + type: integer + GeolocationConfig: + type: object + required: + - country + properties: + city: + type: string + description: Name of the city. Use spaces for multi-word city names. Optional. + state: + type: string + minLength: 2 + maxLength: 2 + description: US state code (2 characters). Must also specify US as the country. Optional. + country: + type: string + minLength: 2 + maxLength: 2 + description: Country code in ISO 3166-1 alpha-2 format + description: Configuration for geolocation + Project: + type: object + required: + - id + - createdAt + - updatedAt + - name + - ownerId + - defaultTimeout + properties: + id: + $ref: "#/components/schemas/uuid" + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + name: + type: string + minLength: 1 + ownerId: + type: string + defaultTimeout: + type: integer + minimum: 60 + maximum: 21600 + ProjectUsage: + type: object + required: + - browserMinutes + - proxyBytes + properties: + browserMinutes: + type: integer + minimum: 0 + proxyBytes: + type: integer + minimum: 0 + Region: + type: string + enum: + - us-west-2 + - us-east-1 + - eu-central-1 + - ap-southeast-1 + Session: + type: object + required: + - id + - createdAt + - updatedAt + - projectId + - startedAt + - expiresAt + - status + - proxyBytes + - keepAlive + - region + properties: + id: + $ref: "#/components/schemas/uuid" + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + projectId: + allOf: + - $ref: "#/components/schemas/uuid" + description: The Project ID linked to the Session. + startedAt: + type: string + format: date-time + endedAt: + type: string + format: date-time + expiresAt: + type: string + format: date-time + status: + $ref: "#/components/schemas/SessionStatus" + proxyBytes: + type: integer + description: Bytes used via the [Proxy](/features/stealth-mode#proxies-and-residential-ips) + avgCpuUsage: + type: integer + description: CPU used by the Session + memoryUsage: + type: integer + description: Memory used by the Session + keepAlive: + type: boolean + description: Indicates if the Session was created to be kept alive upon disconnections + contextId: + allOf: + - $ref: "#/components/schemas/uuid" + description: Optional. The Context linked to the Session. + region: + allOf: + - $ref: "#/components/schemas/Region" + description: The region where the Session is running. + SessionBrowserSettings: + type: object + properties: + context: + $ref: "#/components/schemas/ContextSetting" + extensionId: + allOf: + - $ref: "#/components/schemas/uuid" + description: The uploaded Extension ID. See [Upload Extension](/reference/api/upload-an-extension). + fingerprint: + allOf: + - $ref: "#/components/schemas/Fingerprint" + description: See usage examples [in the Stealth Mode page](/features/stealth-mode#fingerprinting). + viewport: + $ref: "#/components/schemas/SessionBrowserSettingsViewport" + blockAds: + type: boolean + description: Enable or disable ad blocking in the browser. Defaults to `false`. + solveCaptchas: + type: boolean + description: Enable or disable captcha solving in the browser. Defaults to `true`. + recordSession: + type: boolean + description: Enable or disable session recording. Defaults to `true`. + logSession: + type: boolean + description: Enable or disable session logging. Defaults to `true`. + SessionBrowserSettingsViewport: + type: object + properties: + width: + type: integer + height: + type: integer + SessionLiveUrls: + type: object + required: + - debuggerFullscreenUrl + - debuggerUrl + - pages + - wsUrl + properties: + debuggerFullscreenUrl: + type: string + format: uri + debuggerUrl: + type: string + format: uri + pages: + type: array + items: + type: object + properties: + id: + type: string + url: + type: string + format: uri + faviconUrl: + type: string + format: uri + title: + type: string + debuggerUrl: + type: string + format: uri + debuggerFullscreenUrl: + type: string + format: uri + required: + - id + - url + - faviconUrl + - title + - debuggerUrl + - debuggerFullscreenUrl + wsUrl: + type: string + format: uri + SessionLog: + type: object + required: + - eventId + - method + - pageId + - sessionId + - timestamp + properties: + eventId: + type: string + method: + type: string + pageId: + type: integer + request: + $ref: "#/components/schemas/SessionLogRequestBody" + response: + $ref: "#/components/schemas/SessionLogResponseBody" + sessionId: + type: string + timestamp: + type: integer + description: milliseconds that have elapsed since the UNIX epoch + frameId: + type: string + loaderId: + type: string + SessionLogRequestBody: + type: object + required: + - timestamp + - params + - rawBody + properties: + timestamp: + type: integer + description: milliseconds that have elapsed since the UNIX epoch + params: + type: object + additionalProperties: {} + rawBody: + type: string + SessionLogResponseBody: + type: object + required: + - timestamp + - result + - rawBody + properties: + timestamp: + type: integer + description: milliseconds that have elapsed since the UNIX epoch + result: + type: object + additionalProperties: {} + rawBody: + type: string + SessionRecording: + type: object + required: + - id + - data + - sessionId + - timestamp + - type + properties: + id: + type: string + data: + type: object + additionalProperties: {} + description: See [rrweb documentation](https://github.com/rrweb-io/rrweb/blob/master/docs/recipes/dive-into-event.md). + sessionId: + type: string + timestamp: + type: integer + description: milliseconds that have elapsed since the UNIX epoch + type: + type: integer + SessionStatus: + type: string + enum: + - RUNNING + - ERROR + - TIMED_OUT + - COMPLETED + SessionUpdate: + type: object + required: + - projectId + - status + properties: + projectId: + allOf: + - $ref: "#/components/schemas/uuid" + description: The Project ID. Can be found in [Settings](https://www.browserbase.com/settings). + status: + type: string + enum: + - REQUEST_RELEASE + description: Set to `REQUEST_RELEASE` to request that the session complete. Use before session's timeout to avoid additional charges. + Versions: + type: string + enum: + - v1 + uuid: + type: string + securitySchemes: + BrowserbaseAuth: + type: apiKey + in: header + name: X-BB-API-Key + description: Your [Browserbase API Key](https://www.browserbase.com/settings). +servers: + - url: https://www.browserbase.com + description: Public endpoint + variables: {} diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index d298c011..74a99bbb 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -714,4 +714,4 @@ def recording(self) -> AsyncRecordingResourceWithStreamingResponse: @cached_property def uploads(self) -> AsyncUploadsResourceWithStreamingResponse: - return AsyncUploadsResourceWithStreamingResponse(self._sessions.uploads) + return AsyncUploadsResourceWithStreamingResponse(self._sessions.uploads) \ No newline at end of file