diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4029396..c8a8a4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,6 @@ jobs: lint: name: lint runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 @@ -30,6 +29,7 @@ jobs: - name: Run lints run: ./scripts/lint + test: name: test runs-on: ubuntu-latest @@ -50,4 +50,3 @@ jobs: - name: Run tests run: ./scripts/test - diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1214610..2601677 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.0.5" + ".": "1.1.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index d42b050..9a2cf9d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 18 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-7f88912695bab2b98cb73137e6f36125d02fdfaf8eed4532ee1c82385609a259.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-6a5cbe2f816042d594335d77f9600cd47cdb9c21d9d60971a2eca87983061c72.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bb2966..84cd515 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,51 @@ # Changelog +## 1.1.0 (2025-01-24) + +Full Changelog: [v1.0.5...v1.1.0](https://github.com/browserbase/sdk-python/compare/v1.0.5...v1.1.0) + +### Features + +* **api:** api update ([#101](https://github.com/browserbase/sdk-python/issues/101)) ([5be14e9](https://github.com/browserbase/sdk-python/commit/5be14e9b49b95daa2bc043ed8c33b2d4527a7361)) +* **api:** api update ([#104](https://github.com/browserbase/sdk-python/issues/104)) ([c13b2f9](https://github.com/browserbase/sdk-python/commit/c13b2f95924c940deece1f6e3b1e4ca2dfbd9fe7)) +* **api:** api update ([#105](https://github.com/browserbase/sdk-python/issues/105)) ([fc3b82f](https://github.com/browserbase/sdk-python/commit/fc3b82f224e92e273d484f8b0f52eb433210e38b)) +* **api:** api update ([#109](https://github.com/browserbase/sdk-python/issues/109)) ([faca7e9](https://github.com/browserbase/sdk-python/commit/faca7e94c6086d461b81f2806868af2e1506e035)) + + +### Bug Fixes + +* **client:** only call .close() when needed ([#97](https://github.com/browserbase/sdk-python/issues/97)) ([01d5bd5](https://github.com/browserbase/sdk-python/commit/01d5bd5eb7675fc069fe01e7651d769df182270a)) +* correctly handle deserialising `cls` fields ([#100](https://github.com/browserbase/sdk-python/issues/100)) ([b617b85](https://github.com/browserbase/sdk-python/commit/b617b85ef3cce3c16e38125bec483c72bc3d43c0)) + + +### Chores + +* add missing isclass check ([#94](https://github.com/browserbase/sdk-python/issues/94)) ([de5856d](https://github.com/browserbase/sdk-python/commit/de5856dac77567813f681615bef7d147e505a6a0)) +* **internal:** add support for TypeAliasType ([#85](https://github.com/browserbase/sdk-python/issues/85)) ([64448c6](https://github.com/browserbase/sdk-python/commit/64448c6e020aaeb4b39b7ec8f1b28a6b8f0c746a)) +* **internal:** bump httpx dependency ([#95](https://github.com/browserbase/sdk-python/issues/95)) ([d592266](https://github.com/browserbase/sdk-python/commit/d592266e85c40d14e4929089f8ae4db814d04ce7)) +* **internal:** bump pydantic dependency ([#81](https://github.com/browserbase/sdk-python/issues/81)) ([e35a0d8](https://github.com/browserbase/sdk-python/commit/e35a0d85ef0e45aed1a5f58757427bf7c16a76f5)) +* **internal:** bump pyright ([#83](https://github.com/browserbase/sdk-python/issues/83)) ([894b4c4](https://github.com/browserbase/sdk-python/commit/894b4c45b0c36963822923535391aa34dbfec766)) +* **internal:** codegen related update ([#102](https://github.com/browserbase/sdk-python/issues/102)) ([f648bbb](https://github.com/browserbase/sdk-python/commit/f648bbbae4520a1003ecaf5cbd299da9aabfb90f)) +* **internal:** codegen related update ([#106](https://github.com/browserbase/sdk-python/issues/106)) ([3fc9cde](https://github.com/browserbase/sdk-python/commit/3fc9cde212c1ea7f1010c9e688bd75841d828ace)) +* **internal:** codegen related update ([#107](https://github.com/browserbase/sdk-python/issues/107)) ([c97e138](https://github.com/browserbase/sdk-python/commit/c97e1383ac673d05861653c0818c1d1c5b0fa5c8)) +* **internal:** codegen related update ([#86](https://github.com/browserbase/sdk-python/issues/86)) ([ab76578](https://github.com/browserbase/sdk-python/commit/ab76578bdce5eba2410b09f497758fbf0e0d8cf0)) +* **internal:** codegen related update ([#87](https://github.com/browserbase/sdk-python/issues/87)) ([f7f189e](https://github.com/browserbase/sdk-python/commit/f7f189ec317394f2fc532b8f95c3d15304298027)) +* **internal:** codegen related update ([#88](https://github.com/browserbase/sdk-python/issues/88)) ([85f1492](https://github.com/browserbase/sdk-python/commit/85f1492efc58d86ebc34511ca1269a0db2a4d223)) +* **internal:** codegen related update ([#93](https://github.com/browserbase/sdk-python/issues/93)) ([57f0977](https://github.com/browserbase/sdk-python/commit/57f0977c8e050b85b2c2de91202f6775299f80bf)) +* **internal:** codegen related update ([#99](https://github.com/browserbase/sdk-python/issues/99)) ([f817bcb](https://github.com/browserbase/sdk-python/commit/f817bcb67c2080a954c476c15dc048c2c628243a)) +* **internal:** fix some typos ([#92](https://github.com/browserbase/sdk-python/issues/92)) ([51d9f42](https://github.com/browserbase/sdk-python/commit/51d9f42a32d17d2d2277eb8a7b8f35a980c7c485)) +* **internal:** minor formatting changes ([#110](https://github.com/browserbase/sdk-python/issues/110)) ([195c595](https://github.com/browserbase/sdk-python/commit/195c595bfbe2ed97ae4b551658618f4a99a255f0)) +* **internal:** remove some duplicated imports ([#89](https://github.com/browserbase/sdk-python/issues/89)) ([a82ae7d](https://github.com/browserbase/sdk-python/commit/a82ae7d418b1daf68c85e70dea61e628eb785b79)) +* **internal:** updated imports ([#90](https://github.com/browserbase/sdk-python/issues/90)) ([dc6e187](https://github.com/browserbase/sdk-python/commit/dc6e187bfe9585692b2de1b67fc83f027a52c43c)) +* make the `Omit` type public ([#78](https://github.com/browserbase/sdk-python/issues/78)) ([a7bdc57](https://github.com/browserbase/sdk-python/commit/a7bdc57ab7f327da61121986ba7b006238d0e5b5)) + + +### Documentation + +* fix typos ([#98](https://github.com/browserbase/sdk-python/issues/98)) ([d4f4bae](https://github.com/browserbase/sdk-python/commit/d4f4bae46341e91ac537e121bba38e511c7026bc)) +* **readme:** example snippet for client context manager ([#91](https://github.com/browserbase/sdk-python/issues/91)) ([950c8af](https://github.com/browserbase/sdk-python/commit/950c8af19db4581fabd5b965ca4f0af3cc5cd6dc)) +* **readme:** fix http client proxies example ([#82](https://github.com/browserbase/sdk-python/issues/82)) ([cc67c77](https://github.com/browserbase/sdk-python/commit/cc67c773b11b42b406b677f466c7c0ef090b254e)) + ## 1.0.5 (2024-12-03) Full Changelog: [v1.0.4...v1.0.5](https://github.com/browserbase/sdk-python/compare/v1.0.4...v1.0.5) diff --git a/LICENSE b/LICENSE index 915e6f8..2cec9d4 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2024 Browserbase + Copyright 2025 Browserbase Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 1f1c0f9..1a508e6 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ session = client.sessions.create( ) print(session.id) + def run(playwright: Playwright) -> None: # Connect to the remote session chromium = playwright.chromium @@ -51,9 +52,7 @@ def run(playwright: Playwright) -> None: # Execute Playwright actions on the remote browser tab page.goto("https://news.ycombinator.com/") page_title = page.title() - assert ( - page_title == "Hacker News" - ), f"Page title is not 'Hacker News', it is '{page_title}'" + assert page_title == "Hacker News", f"Page title is not 'Hacker News', it is '{page_title}'" page.screenshot(path="screenshot.png") page.close() @@ -121,7 +120,7 @@ except browserbase.APIStatusError as e: print(e.response) ``` -Error codes are as followed: +Error codes are as follows: | Status Code | Error Type | | ----------- | -------------------------- | @@ -260,8 +259,7 @@ If you need to access undocumented endpoints, params, or response properties, th #### Undocumented endpoints To make requests to undocumented endpoints, you can make requests using `client.get`, `client.post`, and other -http verbs. Options on the client will be respected (such as retries) will be respected when making this -request. +http verbs. Options on the client will be respected (such as retries) when making this request. ```py import httpx @@ -290,18 +288,19 @@ can also get all the extra fields on the Pydantic model as a dict with You can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including: -- Support for proxies -- Custom transports +- Support for [proxies](https://www.python-httpx.org/advanced/proxies/) +- Custom [transports](https://www.python-httpx.org/advanced/transports/) - Additional [advanced](https://www.python-httpx.org/advanced/clients/) functionality ```python +import httpx from browserbase import Browserbase, DefaultHttpxClient client = Browserbase( # Or use the `BROWSERBASE_BASE_URL` env var base_url="http://my.test.server.example.com:8083", http_client=DefaultHttpxClient( - proxies="http://my.test.proxy.example.com", + proxy="http://my.test.proxy.example.com", transport=httpx.HTTPTransport(local_address="0.0.0.0"), ), ) @@ -317,12 +316,22 @@ client.with_options(http_client=DefaultHttpxClient(...)) By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting. +```py +from browserbase import Browserbase + +with Browserbase() as client: + # make requests here + ... + +# HTTP client is now closed +``` + ## Versioning This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: 1. Changes that only affect static types, without breaking runtime behavior. -2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals)_. +2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_ 3. Changes that we do not expect to impact the vast majority of users in practice. We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. diff --git a/examples/e2e/test_playwright.py b/examples/e2e/test_playwright.py index 2e58c70..afd94f1 100644 --- a/examples/e2e/test_playwright.py +++ b/examples/e2e/test_playwright.py @@ -29,6 +29,7 @@ def playwright() -> Generator[Playwright, None, None]: with sync_playwright() as p: yield p + def test_playwright_basic(playwright: Playwright) -> None: playwright_basic.run(playwright) diff --git a/examples/playwright_basic.py b/examples/playwright_basic.py index 06fc93c..33bf88a 100644 --- a/examples/playwright_basic.py +++ b/examples/playwright_basic.py @@ -19,9 +19,7 @@ def run(playwright: Playwright) -> None: # Execute Playwright actions on the remote browser tab page.goto("https://news.ycombinator.com/") page_title = page.title() - assert ( - page_title == "Hacker News" - ), f"Page title is not 'Hacker News', it is '{page_title}'" + assert page_title == "Hacker News", f"Page title is not 'Hacker News', it is '{page_title}'" page.screenshot(path="screenshot.png") page.close() diff --git a/examples/playwright_captcha.py b/examples/playwright_captcha.py index 7bc2ff4..980504f 100644 --- a/examples/playwright_captcha.py +++ b/examples/playwright_captcha.py @@ -34,9 +34,7 @@ def handle_console(msg: ConsoleMessage) -> None: page.on("console", handle_console) page.goto(DEFAULT_CAPTCHA_URL, wait_until="networkidle") - page.wait_for_function( - "() => window.captchaSolvingFinished === true", timeout=OVERRIDE_TIMEOUT - ) + 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" diff --git a/examples/playwright_contexts.py b/examples/playwright_contexts.py index 610636f..acf4633 100644 --- a/examples/playwright_contexts.py +++ b/examples/playwright_contexts.py @@ -41,15 +41,11 @@ def run(playwright: Playwright) -> None: # Step 2: Creates a session with the context session = bb.sessions.create( project_id=BROWSERBASE_PROJECT_ID, - browser_settings=TypeAdapter(BrowserSettings).validate_python( - {"context": {"id": context_id, "persist": True}} - ), + browser_settings=TypeAdapter(BrowserSettings).validate_python({"context": {"id": context_id, "persist": True}}), ) print(session) - assert ( - session.context_id == context_id - ), f"Session context_id is {session.context_id}, expected {context_id}" + 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 @@ -90,13 +86,9 @@ def run(playwright: Playwright) -> None: # 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) - ), + 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}" + 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 diff --git a/examples/playwright_extensions.py b/examples/playwright_extensions.py index 7bdb942..6ef1f98 100644 --- a/examples/playwright_extensions.py +++ b/examples/playwright_extensions.py @@ -12,9 +12,7 @@ ) from browserbase.types import Extension, SessionCreateResponse -PATH_TO_EXTENSION = ( - Path.cwd() / "examples" / "packages" / "extensions" / "browserbase-test" -) +PATH_TO_EXTENSION = Path.cwd() / "examples" / "packages" / "extensions" / "browserbase-test" def zip_extension(path: Path = PATH_TO_EXTENSION, save_local: bool = False) -> BytesIO: @@ -23,9 +21,7 @@ def zip_extension(path: Path = PATH_TO_EXTENSION, save_local: bool = False) -> B 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." + 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() @@ -51,9 +47,7 @@ def zip_extension(path: Path = PATH_TO_EXTENSION, save_local: bool = False) -> B def create_extension() -> str: zip_data = zip_extension(save_local=True) - extension: Extension = bb.extensions.create( - file=("extension.zip", zip_data.getvalue()) - ) + extension: Extension = bb.extensions.create(file=("extension.zip", zip_data.getvalue())) return extension.id @@ -75,9 +69,7 @@ def check_for_message(page: Page, message: str) -> None: 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}" + assert message in console_messages, f"Expected message not found in console logs. Messages: {console_messages}" def run(playwright: Playwright) -> None: @@ -141,9 +133,7 @@ def run(playwright: Playwright) -> None: project_id=BROWSERBASE_PROJECT_ID, extension_id=extension_id, ) - raise AssertionError( - "Expected to fail when creating session with deleted extension" - ) + 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)}") diff --git a/examples/playwright_proxy.py b/examples/playwright_proxy.py index 8378e29..47c706b 100644 --- a/examples/playwright_proxy.py +++ b/examples/playwright_proxy.py @@ -11,9 +11,7 @@ def check_proxy_bytes(session_id: str) -> None: - bb.sessions.update( - id=session_id, project_id=BROWSERBASE_PROJECT_ID, status="REQUEST_RELEASE" - ) + 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 ( diff --git a/examples/playwright_upload.py b/examples/playwright_upload.py index da1e32c..6bba6e0 100644 --- a/examples/playwright_upload.py +++ b/examples/playwright_upload.py @@ -33,12 +33,8 @@ def run(playwright: Playwright) -> None: 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}" + 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!") diff --git a/examples/selenium_basic.py b/examples/selenium_basic.py index 83b7307..91c7c32 100644 --- a/examples/selenium_basic.py +++ b/examples/selenium_basic.py @@ -34,7 +34,8 @@ def run() -> None: session = bb.sessions.create(project_id=BROWSERBASE_PROJECT_ID) connection = BrowserbaseConnection(session.id, session.selenium_remote_url) driver = webdriver.Remote( - command_executor=connection, options=webdriver.ChromeOptions() # type: ignore + command_executor=connection, + options=webdriver.ChromeOptions(), # type: ignore ) # Print a bit of info about the browser we've connected to diff --git a/mypy.ini b/mypy.ini index 9e79a7c..811af71 100644 --- a/mypy.ini +++ b/mypy.ini @@ -41,7 +41,7 @@ cache_fine_grained = True # ``` # Changing this codegen to make mypy happy would increase complexity # and would not be worth it. -disable_error_code = func-returns-value +disable_error_code = func-returns-value,overload-cannot-match # https://github.com/python/mypy/issues/12162 [mypy.overrides] diff --git a/pyproject.toml b/pyproject.toml index 82209c4..7e1b0e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "1.0.5" +version = "1.1.0" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" @@ -10,7 +10,7 @@ authors = [ dependencies = [ "httpx>=0.23.0, <1", "pydantic>=1.9.0, <3", - "typing-extensions>=4.7, <5", + "typing-extensions>=4.10, <5", "anyio>=3.5.0, <5", "distro>=1.7.0, <2", "sniffio", @@ -138,6 +138,7 @@ testpaths = ["tests"] addopts = "--tb=short" xfail_strict = true asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "session" filterwarnings = [ "error" ] diff --git a/requirements-dev.lock b/requirements-dev.lock index 48e714f..30dd44d 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -4,7 +4,7 @@ # last locked with the following flags: # pre: false # features: [] -# all-features: false +# all-features: true # with-sources: false # generate-hashes: false # universal: false @@ -48,7 +48,7 @@ h11==0.14.0 # via wsproto httpcore==1.0.6 # via httpx -httpx==0.27.2 +httpx==0.28.1 # via browserbase # via respx idna==3.10 @@ -63,7 +63,7 @@ markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -mypy==1.13.0 +mypy==1.14.1 mypy-extensions==1.0.0 # via mypy nest-asyncio==1.6.0 @@ -81,15 +81,15 @@ playwright==1.48.0 # via pytest-playwright pluggy==1.5.0 # via pytest -pydantic==2.9.2 +pydantic==2.10.3 # via browserbase -pydantic-core==2.23.4 +pydantic-core==2.27.1 # via pydantic pyee==12.0.0 # via playwright pygments==2.18.0 # via rich -pyright==1.1.386 +pyright==1.1.392.post0 pysocks==1.7.1 # via urllib3 pytest==8.3.3 @@ -107,7 +107,7 @@ python-slugify==8.0.4 # via pytest-playwright requests==2.32.3 # via pytest-base-url -respx==0.21.1 +respx==0.22.0 rich==13.9.3 ruff==0.7.1 selenium==4.25.0 @@ -116,7 +116,6 @@ six==1.16.0 sniffio==1.3.1 # via anyio # via browserbase - # via httpx # via trio sortedcontainers==2.4.0 # via trio diff --git a/requirements.lock b/requirements.lock index 4bff3dd..9efa54d 100644 --- a/requirements.lock +++ b/requirements.lock @@ -26,19 +26,18 @@ h11==0.14.0 # via httpcore httpcore==1.0.2 # via httpx -httpx==0.25.2 +httpx==0.28.1 # via browserbase idna==3.4 # via anyio # via httpx -pydantic==2.9.2 +pydantic==2.10.3 # via browserbase -pydantic-core==2.23.4 +pydantic-core==2.27.1 # via pydantic sniffio==1.3.0 # via anyio # via browserbase - # via httpx typing-extensions==4.12.2 # via anyio # via browserbase diff --git a/scripts/bootstrap b/scripts/bootstrap index 8c5c60e..e84fe62 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,7 +4,7 @@ set -e cd "$(dirname "$0")/.." -if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then +if ! command -v rye >/dev/null 2>&1 && [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then brew bundle check >/dev/null 2>&1 || { echo "==> Installing Homebrew dependencies…" brew bundle diff --git a/scripts/lint b/scripts/lint index a74a198..feccbdd 100755 --- a/scripts/lint +++ b/scripts/lint @@ -9,4 +9,3 @@ rye run lint echo "==> Making sure it imports" rye run python -c 'import browserbase' - diff --git a/src/browserbase/__init__.py b/src/browserbase/__init__.py index 4b1d280..ce50c4e 100644 --- a/src/browserbase/__init__.py +++ b/src/browserbase/__init__.py @@ -1,7 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from . import types -from ._types import NOT_GIVEN, NoneType, NotGiven, Transport, ProxiesTypes +from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes from ._utils import file_from_path from ._client import ( Client, @@ -46,6 +46,7 @@ "ProxiesTypes", "NotGiven", "NOT_GIVEN", + "Omit", "BrowserbaseError", "APIError", "APIStatusError", diff --git a/src/browserbase/_base_client.py b/src/browserbase/_base_client.py index d8b28d9..b21f479 100644 --- a/src/browserbase/_base_client.py +++ b/src/browserbase/_base_client.py @@ -767,6 +767,9 @@ def __init__(self, **kwargs: Any) -> None: class SyncHttpxClientWrapper(DefaultHttpxClient): def __del__(self) -> None: + if self.is_closed: + return + try: self.close() except Exception: @@ -1334,6 +1337,9 @@ def __init__(self, **kwargs: Any) -> None: class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient): def __del__(self) -> None: + if self.is_closed: + return + try: # TODO(someday): support non asyncio runtimes here asyncio.get_running_loop().create_task(self.aclose()) diff --git a/src/browserbase/_client.py b/src/browserbase/_client.py index b3ed32f..b845ea8 100644 --- a/src/browserbase/_client.py +++ b/src/browserbase/_client.py @@ -8,7 +8,7 @@ import httpx -from . import resources, _exceptions +from . import _exceptions from ._qs import Querystring from ._types import ( NOT_GIVEN, @@ -24,6 +24,7 @@ get_async_library, ) from ._version import __version__ +from .resources import contexts, projects, extensions from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import APIStatusError, BrowserbaseError from ._base_client import ( @@ -31,13 +32,13 @@ SyncAPIClient, AsyncAPIClient, ) +from .resources.sessions import sessions __all__ = [ "Timeout", "Transport", "ProxiesTypes", "RequestOptions", - "resources", "Browserbase", "AsyncBrowserbase", "Client", @@ -46,10 +47,10 @@ class Browserbase(SyncAPIClient): - contexts: resources.ContextsResource - extensions: resources.ExtensionsResource - projects: resources.ProjectsResource - sessions: resources.SessionsResource + contexts: contexts.ContextsResource + extensions: extensions.ExtensionsResource + projects: projects.ProjectsResource + sessions: sessions.SessionsResource with_raw_response: BrowserbaseWithRawResponse with_streaming_response: BrowserbaseWithStreamedResponse @@ -107,10 +108,10 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.contexts = resources.ContextsResource(self) - self.extensions = resources.ExtensionsResource(self) - self.projects = resources.ProjectsResource(self) - self.sessions = resources.SessionsResource(self) + self.contexts = contexts.ContextsResource(self) + self.extensions = extensions.ExtensionsResource(self) + self.projects = projects.ProjectsResource(self) + self.sessions = sessions.SessionsResource(self) self.with_raw_response = BrowserbaseWithRawResponse(self) self.with_streaming_response = BrowserbaseWithStreamedResponse(self) @@ -220,10 +221,10 @@ def _make_status_error( class AsyncBrowserbase(AsyncAPIClient): - contexts: resources.AsyncContextsResource - extensions: resources.AsyncExtensionsResource - projects: resources.AsyncProjectsResource - sessions: resources.AsyncSessionsResource + contexts: contexts.AsyncContextsResource + extensions: extensions.AsyncExtensionsResource + projects: projects.AsyncProjectsResource + sessions: sessions.AsyncSessionsResource with_raw_response: AsyncBrowserbaseWithRawResponse with_streaming_response: AsyncBrowserbaseWithStreamedResponse @@ -281,10 +282,10 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.contexts = resources.AsyncContextsResource(self) - self.extensions = resources.AsyncExtensionsResource(self) - self.projects = resources.AsyncProjectsResource(self) - self.sessions = resources.AsyncSessionsResource(self) + self.contexts = contexts.AsyncContextsResource(self) + self.extensions = extensions.AsyncExtensionsResource(self) + self.projects = projects.AsyncProjectsResource(self) + self.sessions = sessions.AsyncSessionsResource(self) self.with_raw_response = AsyncBrowserbaseWithRawResponse(self) self.with_streaming_response = AsyncBrowserbaseWithStreamedResponse(self) @@ -395,34 +396,34 @@ def _make_status_error( class BrowserbaseWithRawResponse: def __init__(self, client: Browserbase) -> None: - self.contexts = resources.ContextsResourceWithRawResponse(client.contexts) - self.extensions = resources.ExtensionsResourceWithRawResponse(client.extensions) - self.projects = resources.ProjectsResourceWithRawResponse(client.projects) - self.sessions = resources.SessionsResourceWithRawResponse(client.sessions) + self.contexts = contexts.ContextsResourceWithRawResponse(client.contexts) + self.extensions = extensions.ExtensionsResourceWithRawResponse(client.extensions) + self.projects = projects.ProjectsResourceWithRawResponse(client.projects) + self.sessions = sessions.SessionsResourceWithRawResponse(client.sessions) class AsyncBrowserbaseWithRawResponse: def __init__(self, client: AsyncBrowserbase) -> None: - self.contexts = resources.AsyncContextsResourceWithRawResponse(client.contexts) - self.extensions = resources.AsyncExtensionsResourceWithRawResponse(client.extensions) - self.projects = resources.AsyncProjectsResourceWithRawResponse(client.projects) - self.sessions = resources.AsyncSessionsResourceWithRawResponse(client.sessions) + self.contexts = contexts.AsyncContextsResourceWithRawResponse(client.contexts) + self.extensions = extensions.AsyncExtensionsResourceWithRawResponse(client.extensions) + self.projects = projects.AsyncProjectsResourceWithRawResponse(client.projects) + self.sessions = sessions.AsyncSessionsResourceWithRawResponse(client.sessions) class BrowserbaseWithStreamedResponse: def __init__(self, client: Browserbase) -> None: - self.contexts = resources.ContextsResourceWithStreamingResponse(client.contexts) - self.extensions = resources.ExtensionsResourceWithStreamingResponse(client.extensions) - self.projects = resources.ProjectsResourceWithStreamingResponse(client.projects) - self.sessions = resources.SessionsResourceWithStreamingResponse(client.sessions) + self.contexts = contexts.ContextsResourceWithStreamingResponse(client.contexts) + self.extensions = extensions.ExtensionsResourceWithStreamingResponse(client.extensions) + self.projects = projects.ProjectsResourceWithStreamingResponse(client.projects) + self.sessions = sessions.SessionsResourceWithStreamingResponse(client.sessions) class AsyncBrowserbaseWithStreamedResponse: def __init__(self, client: AsyncBrowserbase) -> None: - self.contexts = resources.AsyncContextsResourceWithStreamingResponse(client.contexts) - self.extensions = resources.AsyncExtensionsResourceWithStreamingResponse(client.extensions) - self.projects = resources.AsyncProjectsResourceWithStreamingResponse(client.projects) - self.sessions = resources.AsyncSessionsResourceWithStreamingResponse(client.sessions) + self.contexts = contexts.AsyncContextsResourceWithStreamingResponse(client.contexts) + self.extensions = extensions.AsyncExtensionsResourceWithStreamingResponse(client.extensions) + self.projects = projects.AsyncProjectsResourceWithStreamingResponse(client.projects) + self.sessions = sessions.AsyncSessionsResourceWithStreamingResponse(client.sessions) Client = Browserbase diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index 6cb469e..9a918aa 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -46,6 +46,7 @@ strip_not_given, extract_type_arg, is_annotated_type, + is_type_alias_type, strip_annotated_type, ) from ._compat import ( @@ -178,14 +179,14 @@ def __str__(self) -> str: @classmethod @override def construct( # pyright: ignore[reportIncompatibleMethodOverride] - cls: Type[ModelT], + __cls: Type[ModelT], _fields_set: set[str] | None = None, **values: object, ) -> ModelT: - m = cls.__new__(cls) + m = __cls.__new__(__cls) fields_values: dict[str, object] = {} - config = get_model_config(cls) + config = get_model_config(__cls) populate_by_name = ( config.allow_population_by_field_name if isinstance(config, _ConfigProtocol) @@ -195,7 +196,7 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride] if _fields_set is None: _fields_set = set() - model_fields = get_model_fields(cls) + model_fields = get_model_fields(__cls) for name, field in model_fields.items(): key = field.alias if key is None or (key not in values and populate_by_name): @@ -428,6 +429,8 @@ def construct_type(*, value: object, type_: object) -> object: # we allow `object` as the input type because otherwise, passing things like # `Literal['value']` will be reported as a type error by type checkers type_ = cast("type[object]", type_) + if is_type_alias_type(type_): + type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` if is_annotated_type(type_): @@ -485,7 +488,11 @@ def construct_type(*, value: object, type_: object) -> object: _, items_type = get_args(type_) # Dict[_, items_type] return {key: construct_type(value=item, type_=items_type) for key, item in value.items()} - if not is_literal_type(type_) and (issubclass(origin, BaseModel) or issubclass(origin, GenericModel)): + if ( + not is_literal_type(type_) + and inspect.isclass(origin) + and (issubclass(origin, BaseModel) or issubclass(origin, GenericModel)) + ): if is_list(value): return [cast(Any, type_).construct(**entry) if is_mapping(entry) else entry for entry in value] diff --git a/src/browserbase/_response.py b/src/browserbase/_response.py index 81ae082..e79cb15 100644 --- a/src/browserbase/_response.py +++ b/src/browserbase/_response.py @@ -25,7 +25,7 @@ import pydantic from ._types import NoneType -from ._utils import is_given, extract_type_arg, is_annotated_type, extract_type_var_from_base +from ._utils import is_given, extract_type_arg, is_annotated_type, is_type_alias_type, extract_type_var_from_base from ._models import BaseModel, is_basemodel from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type @@ -126,9 +126,17 @@ def __repr__(self) -> str: ) def _parse(self, *, to: type[_T] | None = None) -> R | _T: + cast_to = to if to is not None else self._cast_to + + # unwrap `TypeAlias('Name', T)` -> `T` + if is_type_alias_type(cast_to): + cast_to = cast_to.__value__ # type: ignore[unreachable] + # unwrap `Annotated[T, ...]` -> `T` - if to and is_annotated_type(to): - to = extract_type_arg(to, 0) + if cast_to and is_annotated_type(cast_to): + cast_to = extract_type_arg(cast_to, 0) + + origin = get_origin(cast_to) or cast_to if self._is_sse_stream: if to: @@ -164,18 +172,12 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: return cast( R, stream_cls( - cast_to=self._cast_to, + cast_to=cast_to, response=self.http_response, client=cast(Any, self._client), ), ) - cast_to = to if to is not None else self._cast_to - - # unwrap `Annotated[T, ...]` -> `T` - if is_annotated_type(cast_to): - cast_to = extract_type_arg(cast_to, 0) - if cast_to is NoneType: return cast(R, None) @@ -195,8 +197,6 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: if cast_to == bool: return cast(R, response.text.lower() == "true") - origin = get_origin(cast_to) or cast_to - if origin == APIResponse: raise RuntimeError("Unexpected state - cast_to is `APIResponse`") @@ -210,7 +210,13 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: raise ValueError(f"Subclasses of httpx.Response cannot be passed to `cast_to`") return cast(R, response) - if inspect.isclass(origin) and not issubclass(origin, BaseModel) and issubclass(origin, pydantic.BaseModel): + if ( + inspect.isclass( + origin # pyright: ignore[reportUnknownArgumentType] + ) + and not issubclass(origin, BaseModel) + and issubclass(origin, pydantic.BaseModel) + ): raise TypeError( "Pydantic models must subclass our base model type, e.g. `from browserbase import BaseModel`" ) diff --git a/src/browserbase/_types.py b/src/browserbase/_types.py index 1691090..a8833dc 100644 --- a/src/browserbase/_types.py +++ b/src/browserbase/_types.py @@ -192,10 +192,8 @@ def get(self, __key: str) -> str | None: ... StrBytesIntFloat = Union[str, bytes, int, float] # Note: copied from Pydantic -# https://github.com/pydantic/pydantic/blob/32ea570bf96e84234d2992e1ddf40ab8a565925a/pydantic/main.py#L49 -IncEx: TypeAlias = Union[ - Set[int], Set[str], Mapping[int, Union["IncEx", Literal[True]]], Mapping[str, Union["IncEx", Literal[True]]] -] +# https://github.com/pydantic/pydantic/blob/6f31f8f68ef011f84357330186f603ff295312fd/pydantic/main.py#L79 +IncEx: TypeAlias = Union[Set[int], Set[str], Mapping[int, Union["IncEx", bool]], Mapping[str, Union["IncEx", bool]]] PostParser = Callable[[Any], Any] diff --git a/src/browserbase/_utils/__init__.py b/src/browserbase/_utils/__init__.py index a7cff3c..d4fda26 100644 --- a/src/browserbase/_utils/__init__.py +++ b/src/browserbase/_utils/__init__.py @@ -39,6 +39,7 @@ is_iterable_type as is_iterable_type, is_required_type as is_required_type, is_annotated_type as is_annotated_type, + is_type_alias_type as is_type_alias_type, strip_annotated_type as strip_annotated_type, extract_type_var_from_base as extract_type_var_from_base, ) diff --git a/src/browserbase/_utils/_typing.py b/src/browserbase/_utils/_typing.py index c036991..278749b 100644 --- a/src/browserbase/_utils/_typing.py +++ b/src/browserbase/_utils/_typing.py @@ -1,8 +1,17 @@ from __future__ import annotations +import sys +import typing +import typing_extensions from typing import Any, TypeVar, Iterable, cast from collections import abc as _c_abc -from typing_extensions import Required, Annotated, get_args, get_origin +from typing_extensions import ( + TypeIs, + Required, + Annotated, + get_args, + get_origin, +) from .._types import InheritsGeneric from .._compat import is_union as _is_union @@ -36,6 +45,26 @@ def is_typevar(typ: type) -> bool: return type(typ) == TypeVar # type: ignore +_TYPE_ALIAS_TYPES: tuple[type[typing_extensions.TypeAliasType], ...] = (typing_extensions.TypeAliasType,) +if sys.version_info >= (3, 12): + _TYPE_ALIAS_TYPES = (*_TYPE_ALIAS_TYPES, typing.TypeAliasType) + + +def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]: + """Return whether the provided argument is an instance of `TypeAliasType`. + + ```python + type Int = int + is_type_alias_type(Int) + # > True + Str = TypeAliasType("Str", str) + is_type_alias_type(Str) + # > True + ``` + """ + return isinstance(tp, _TYPE_ALIAS_TYPES) + + # Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]] def strip_annotated_type(typ: type) -> type: if is_required_type(typ) or is_annotated_type(typ): diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index c871d01..9621169 100644 --- a/src/browserbase/_version.py +++ b/src/browserbase/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "browserbase" -__version__ = "1.0.5" # x-release-please-version +__version__ = "1.1.0" # x-release-please-version diff --git a/src/browserbase/resources/contexts.py b/src/browserbase/resources/contexts.py index 806cb01..486cd5f 100644 --- a/src/browserbase/resources/contexts.py +++ b/src/browserbase/resources/contexts.py @@ -30,7 +30,7 @@ class ContextsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> ContextsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers @@ -153,7 +153,7 @@ class AsyncContextsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncContextsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers diff --git a/src/browserbase/resources/extensions.py b/src/browserbase/resources/extensions.py index dc6c0ac..a98685a 100644 --- a/src/browserbase/resources/extensions.py +++ b/src/browserbase/resources/extensions.py @@ -32,7 +32,7 @@ class ExtensionsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> ExtensionsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers @@ -159,7 +159,7 @@ class AsyncExtensionsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncExtensionsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers diff --git a/src/browserbase/resources/projects.py b/src/browserbase/resources/projects.py index bf4a5df..fb337a0 100644 --- a/src/browserbase/resources/projects.py +++ b/src/browserbase/resources/projects.py @@ -25,7 +25,7 @@ class ProjectsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> ProjectsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers @@ -131,7 +131,7 @@ class AsyncProjectsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncProjectsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers diff --git a/src/browserbase/resources/sessions/downloads.py b/src/browserbase/resources/sessions/downloads.py index 461163b..9ee4975 100644 --- a/src/browserbase/resources/sessions/downloads.py +++ b/src/browserbase/resources/sessions/downloads.py @@ -26,7 +26,7 @@ class DownloadsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> DownloadsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers @@ -81,7 +81,7 @@ class AsyncDownloadsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncDownloadsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers diff --git a/src/browserbase/resources/sessions/logs.py b/src/browserbase/resources/sessions/logs.py index 07fb581..2a42c9d 100644 --- a/src/browserbase/resources/sessions/logs.py +++ b/src/browserbase/resources/sessions/logs.py @@ -23,7 +23,7 @@ class LogsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> LogsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers @@ -77,7 +77,7 @@ class AsyncLogsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncLogsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers diff --git a/src/browserbase/resources/sessions/recording.py b/src/browserbase/resources/sessions/recording.py index b216fd9..856b292 100644 --- a/src/browserbase/resources/sessions/recording.py +++ b/src/browserbase/resources/sessions/recording.py @@ -23,7 +23,7 @@ class RecordingResource(SyncAPIResource): @cached_property def with_raw_response(self) -> RecordingResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers @@ -77,7 +77,7 @@ class AsyncRecordingResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncRecordingResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index 45ef082..0572d91 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Union, Iterable +from typing import Dict, Union, Iterable from typing_extensions import Literal import httpx @@ -82,7 +82,7 @@ def uploads(self) -> UploadsResource: @cached_property def with_raw_response(self) -> SessionsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers @@ -108,6 +108,7 @@ def create( proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | NotGiven = NOT_GIVEN, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | NotGiven = NOT_GIVEN, api_timeout: int | NotGiven = NOT_GIVEN, + user_metadata: Dict[str, object] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -137,6 +138,9 @@ def create( api_timeout: Duration in seconds after which the session will automatically end. Defaults to the Project's `defaultTimeout`. + user_metadata: Arbitrary user metadata to attach to the session. To learn more about user + metadata, see [User Metadata](/features/sessions#user-metadata). + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -156,6 +160,7 @@ def create( "proxies": proxies, "region": region, "api_timeout": api_timeout, + "user_metadata": user_metadata, }, session_create_params.SessionCreateParams, ), @@ -250,6 +255,7 @@ def update( def list( self, *, + q: str | NotGiven = NOT_GIVEN, status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -258,10 +264,15 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> SessionListResponse: - """ - List Sessions + """List Sessions Args: + q: Query sessions by user metadata. + + See + [Querying Sessions by User Metadata](/features/sessions#querying-sessions-by-user-metadata) + for the schema of this query. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -277,7 +288,13 @@ def list( extra_query=extra_query, extra_body=extra_body, timeout=timeout, - query=maybe_transform({"status": status}, session_list_params.SessionListParams), + query=maybe_transform( + { + "q": q, + "status": status, + }, + session_list_params.SessionListParams, + ), ), cast_to=SessionListResponse, ) @@ -336,7 +353,7 @@ def uploads(self) -> AsyncUploadsResource: @cached_property def with_raw_response(self) -> AsyncSessionsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers @@ -362,6 +379,7 @@ async def create( proxies: Union[bool, Iterable[session_create_params.ProxiesUnionMember1]] | NotGiven = NOT_GIVEN, region: Literal["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"] | NotGiven = NOT_GIVEN, api_timeout: int | NotGiven = NOT_GIVEN, + user_metadata: Dict[str, object] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -391,6 +409,9 @@ async def create( api_timeout: Duration in seconds after which the session will automatically end. Defaults to the Project's `defaultTimeout`. + user_metadata: Arbitrary user metadata to attach to the session. To learn more about user + metadata, see [User Metadata](/features/sessions#user-metadata). + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -410,6 +431,7 @@ async def create( "proxies": proxies, "region": region, "api_timeout": api_timeout, + "user_metadata": user_metadata, }, session_create_params.SessionCreateParams, ), @@ -504,6 +526,7 @@ async def update( async def list( self, *, + q: str | NotGiven = NOT_GIVEN, status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -512,10 +535,15 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> SessionListResponse: - """ - List Sessions + """List Sessions Args: + q: Query sessions by user metadata. + + See + [Querying Sessions by User Metadata](/features/sessions#querying-sessions-by-user-metadata) + for the schema of this query. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -531,7 +559,13 @@ async def list( extra_query=extra_query, extra_body=extra_body, timeout=timeout, - query=await async_maybe_transform({"status": status}, session_list_params.SessionListParams), + query=await async_maybe_transform( + { + "q": q, + "status": status, + }, + session_list_params.SessionListParams, + ), ), cast_to=SessionListResponse, ) @@ -715,4 +749,4 @@ def recording(self) -> AsyncRecordingResourceWithStreamingResponse: @cached_property def uploads(self) -> AsyncUploadsResourceWithStreamingResponse: - return AsyncUploadsResourceWithStreamingResponse(self._sessions.uploads) \ No newline at end of file + return AsyncUploadsResourceWithStreamingResponse(self._sessions.uploads) diff --git a/src/browserbase/resources/sessions/uploads.py b/src/browserbase/resources/sessions/uploads.py index e985e4d..eed9349 100644 --- a/src/browserbase/resources/sessions/uploads.py +++ b/src/browserbase/resources/sessions/uploads.py @@ -32,7 +32,7 @@ class UploadsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> UploadsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers @@ -95,7 +95,7 @@ class AsyncUploadsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncUploadsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/browserbase/sdk-python#accessing-raw-response-data-eg-headers diff --git a/src/browserbase/types/session.py b/src/browserbase/types/session.py index 8bd47f9..16450e2 100644 --- a/src/browserbase/types/session.py +++ b/src/browserbase/types/session.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import Dict, Optional from datetime import datetime from typing_extensions import Literal @@ -46,3 +46,10 @@ class Session(BaseModel): memory_usage: Optional[int] = FieldInfo(alias="memoryUsage", default=None) """Memory used by the Session""" + + user_metadata: Optional[Dict[str, object]] = FieldInfo(alias="userMetadata", default=None) + """Arbitrary user metadata to attach to the session. + + To learn more about user metadata, see + [User Metadata](/features/sessions#user-metadata). + """ diff --git a/src/browserbase/types/session_create_params.py b/src/browserbase/types/session_create_params.py index bd643b3..5e76037 100644 --- a/src/browserbase/types/session_create_params.py +++ b/src/browserbase/types/session_create_params.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Union, Iterable +from typing import Dict, List, Union, Iterable from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict from .._utils import PropertyInfo @@ -57,6 +57,13 @@ class SessionCreateParams(TypedDict, total=False): Defaults to the Project's `defaultTimeout`. """ + user_metadata: Annotated[Dict[str, object], PropertyInfo(alias="userMetadata")] + """Arbitrary user metadata to attach to the session. + + To learn more about user metadata, see + [User Metadata](/features/sessions#user-metadata). + """ + class BrowserSettingsContext(TypedDict, total=False): id: Required[str] @@ -107,6 +114,9 @@ class BrowserSettingsViewport(TypedDict, total=False): class BrowserSettings(TypedDict, total=False): + advanced_stealth: Annotated[bool, PropertyInfo(alias="advancedStealth")] + """Advanced Browser Stealth Mode""" + block_ads: Annotated[bool, PropertyInfo(alias="blockAds")] """Enable or disable ad blocking in the browser. Defaults to `false`.""" diff --git a/src/browserbase/types/session_create_response.py b/src/browserbase/types/session_create_response.py index 8c9ae09..b548d50 100644 --- a/src/browserbase/types/session_create_response.py +++ b/src/browserbase/types/session_create_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import Dict, Optional from datetime import datetime from typing_extensions import Literal @@ -55,3 +55,10 @@ class SessionCreateResponse(BaseModel): memory_usage: Optional[int] = FieldInfo(alias="memoryUsage", default=None) """Memory used by the Session""" + + user_metadata: Optional[Dict[str, object]] = FieldInfo(alias="userMetadata", default=None) + """Arbitrary user metadata to attach to the session. + + To learn more about user metadata, see + [User Metadata](/features/sessions#user-metadata). + """ diff --git a/src/browserbase/types/session_list_params.py b/src/browserbase/types/session_list_params.py index 7ba4798..54b0a05 100644 --- a/src/browserbase/types/session_list_params.py +++ b/src/browserbase/types/session_list_params.py @@ -8,4 +8,12 @@ class SessionListParams(TypedDict, total=False): + q: str + """Query sessions by user metadata. + + See + [Querying Sessions by User Metadata](/features/sessions#querying-sessions-by-user-metadata) + for the schema of this query. + """ + status: Literal["RUNNING", "ERROR", "TIMED_OUT", "COMPLETED"] diff --git a/src/browserbase/types/sessions/session_log.py b/src/browserbase/types/sessions/session_log.py index d15eb83..af58030 100644 --- a/src/browserbase/types/sessions/session_log.py +++ b/src/browserbase/types/sessions/session_log.py @@ -28,8 +28,6 @@ class Response(BaseModel): class SessionLog(BaseModel): - event_id: str = FieldInfo(alias="eventId") - method: str page_id: int = FieldInfo(alias="pageId") diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 7b9fbce..482624f 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -34,6 +34,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: session = client.sessions.create( project_id="projectId", browser_settings={ + "advanced_stealth": True, "block_ads": True, "context": { "id": "id", @@ -66,6 +67,7 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: proxies=True, region="us-west-2", api_timeout=60, + user_metadata={"foo": "bar"}, ) assert_matches_type(SessionCreateResponse, session, path=["response"]) @@ -185,6 +187,7 @@ def test_method_list(self, client: Browserbase) -> None: @parametrize def test_method_list_with_all_params(self, client: Browserbase) -> None: session = client.sessions.list( + q="q", status="RUNNING", ) assert_matches_type(SessionListResponse, session, path=["response"]) @@ -263,6 +266,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas session = await async_client.sessions.create( project_id="projectId", browser_settings={ + "advanced_stealth": True, "block_ads": True, "context": { "id": "id", @@ -295,6 +299,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas proxies=True, region="us-west-2", api_timeout=60, + user_metadata={"foo": "bar"}, ) assert_matches_type(SessionCreateResponse, session, path=["response"]) @@ -414,6 +419,7 @@ async def test_method_list(self, async_client: AsyncBrowserbase) -> None: @parametrize async def test_method_list_with_all_params(self, async_client: AsyncBrowserbase) -> None: session = await async_client.sessions.list( + q="q", status="RUNNING", ) assert_matches_type(SessionListResponse, session, path=["response"]) diff --git a/tests/test_client.py b/tests/test_client.py index aa0043e..20ac94b 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -6,6 +6,7 @@ import os import sys import json +import time import asyncio import inspect import subprocess @@ -355,11 +356,11 @@ def test_default_query_option(self) -> None: FinalRequestOptions( method="get", url="/foo", - params={"foo": "baz", "query_param": "overriden"}, + params={"foo": "baz", "query_param": "overridden"}, ) ) url = httpx.URL(request.url) - assert dict(url.params) == {"foo": "baz", "query_param": "overriden"} + assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} def test_request_extra_json(self) -> None: request = self.client._build_request( @@ -1131,11 +1132,11 @@ def test_default_query_option(self) -> None: FinalRequestOptions( method="get", url="/foo", - params={"foo": "baz", "query_param": "overriden"}, + params={"foo": "baz", "query_param": "overridden"}, ) ) url = httpx.URL(request.url) - assert dict(url.params) == {"foo": "baz", "query_param": "overriden"} + assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} def test_request_extra_json(self) -> None: request = self.client._build_request( @@ -1639,10 +1640,20 @@ async def test_main() -> None: [sys.executable, "-c", test_code], text=True, ) as process: - try: - process.wait(2) - if process.returncode: - raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code") - except subprocess.TimeoutExpired as e: - process.kill() - raise AssertionError("calling get_platform using asyncify resulted in a hung process") from e + timeout = 10 # seconds + + start_time = time.monotonic() + while True: + return_code = process.poll() + if return_code is not None: + if return_code != 0: + raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code") + + # success + break + + if time.monotonic() - start_time > timeout: + process.kill() + raise AssertionError("calling get_platform using asyncify resulted in a hung process") + + time.sleep(0.1) diff --git a/tests/test_models.py b/tests/test_models.py index 37671b0..669d219 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,7 @@ import json from typing import Any, Dict, List, Union, Optional, cast from datetime import datetime, timezone -from typing_extensions import Literal, Annotated +from typing_extensions import Literal, Annotated, TypeAliasType import pytest import pydantic @@ -828,3 +828,29 @@ class B(BaseModel): # if the discriminator details object stays the same between invocations then # we hit the cache assert UnionType.__discriminator__ is discriminator + + +@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") +def test_type_alias_type() -> None: + Alias = TypeAliasType("Alias", str) + + class Model(BaseModel): + alias: Alias + union: Union[int, Alias] + + m = construct_type(value={"alias": "foo", "union": "bar"}, type_=Model) + assert isinstance(m, Model) + assert isinstance(m.alias, str) + assert m.alias == "foo" + assert isinstance(m.union, str) + assert m.union == "bar" + + +@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") +def test_field_named_cls() -> None: + class Model(BaseModel): + cls: str + + m = construct_type(value={"cls": "foo"}, type_=Model) + assert isinstance(m, Model) + assert isinstance(m.cls, str) diff --git a/tests/utils.py b/tests/utils.py index ad9be37..ac183a7 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -16,6 +16,7 @@ is_union_type, extract_type_arg, is_annotated_type, + is_type_alias_type, ) from browserbase._compat import PYDANTIC_V2, field_outer_type, get_model_fields from browserbase._models import BaseModel @@ -51,6 +52,9 @@ def assert_matches_type( path: list[str], allow_none: bool = False, ) -> None: + if is_type_alias_type(type_): + type_ = type_.__value__ + # unwrap `Annotated[T, ...]` -> `T` if is_annotated_type(type_): type_ = extract_type_arg(type_, 0)