diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2ebc4e90..f80372ae 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.19.5" + ".": "3.20.0" } diff --git a/.stats.yml b/.stats.yml index ade26eb1..d8428d42 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 8 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-b969ce378479c79ee64c05127c0ed6c6ce2edbee017ecd037242fb618a5ebc9f.yml -openapi_spec_hash: a24aabaa5214effb679808b7f2be0ad4 -config_hash: 0cc516caf1432087f40654336e0fa8cd +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-1c6caa2891a7f3bdfc0caab143f285badc9145220c9b29cd5e4cf1a9b3ac11cf.yml +openapi_spec_hash: 28c4b734a5309067c39bb4c4b709b9ab +config_hash: a962ae71493deb11a1c903256fb25386 diff --git a/CHANGELOG.md b/CHANGELOG.md index bbaf6ba1..510bf324 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 3.20.0 (2026-04-11) + +Full Changelog: [v3.19.5...v3.20.0](https://github.com/browserbase/stagehand-python/compare/v3.19.5...v3.20.0) + +### Features + +* [STG-1798] feat: support Browserbase verified sessions ([078ab5c](https://github.com/browserbase/stagehand-python/commit/078ab5c76f13beee16e44d7eed5e3018f1cc5bd8)) +* Bedrock auth passthrough ([9463fa4](https://github.com/browserbase/stagehand-python/commit/9463fa49cb839abbb2c6a1adb0d053e5006216a7)) +* Revert "[STG-1573] Add providerOptions for extensible model auth ([#1822](https://github.com/browserbase/stagehand-python/issues/1822))" ([3c04086](https://github.com/browserbase/stagehand-python/commit/3c0408675154c9f7d241c4e92e9cb82f0419d6b3)) + + +### Bug Fixes + +* **client:** preserve hardcoded query params when merging with user params ([feac39d](https://github.com/browserbase/stagehand-python/commit/feac39d88a841ff710d672b622f3280037589f66)) +* ensure file data are only sent as 1 parameter ([b870657](https://github.com/browserbase/stagehand-python/commit/b8706575ab0f95b9e6781ee3685f9b79e0fe6036)) + ## 3.19.5 (2026-04-03) Full Changelog: [v3.19.4...v3.19.5](https://github.com/browserbase/stagehand-python/compare/v3.19.4...v3.19.5) diff --git a/pyproject.toml b/pyproject.toml index 96fa8963..d3411cde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stagehand" -version = "3.19.5" +version = "3.20.0" description = "The official Python library for the stagehand API" dynamic = ["readme"] license = "MIT" diff --git a/src/stagehand/_base_client.py b/src/stagehand/_base_client.py index 951feb82..9af0033b 100644 --- a/src/stagehand/_base_client.py +++ b/src/stagehand/_base_client.py @@ -540,6 +540,10 @@ def _build_request( files = cast(HttpxRequestFiles, ForceMultipartDict()) prepared_url = self._prepare_url(options.url) + # preserve hard-coded query params from the url + if params and prepared_url.query: + params = {**dict(prepared_url.params.items()), **params} + prepared_url = prepared_url.copy_with(raw_path=prepared_url.raw_path.split(b"?", 1)[0]) if "_" in prepared_url.host: # work around https://github.com/encode/httpx/discussions/2880 kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} diff --git a/src/stagehand/_utils/_utils.py b/src/stagehand/_utils/_utils.py index 1c50ff6a..b49804f3 100644 --- a/src/stagehand/_utils/_utils.py +++ b/src/stagehand/_utils/_utils.py @@ -86,8 +86,9 @@ def _extract_items( index += 1 if is_dict(obj): try: - # We are at the last entry in the path so we must remove the field - if (len(path)) == index: + # Remove the field if there are no more dict keys in the path, + # only "" traversal markers or end. + if all(p == "" for p in path[index:]): item = obj.pop(key) else: item = obj[key] diff --git a/src/stagehand/_version.py b/src/stagehand/_version.py index 15e655a3..b5e0b8a6 100644 --- a/src/stagehand/_version.py +++ b/src/stagehand/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "stagehand" -__version__ = "3.19.5" # x-release-please-version +__version__ = "3.20.0" # x-release-please-version diff --git a/src/stagehand/types/session_start_params.py b/src/stagehand/types/session_start_params.py index 17f55997..5123c507 100644 --- a/src/stagehand/types/session_start_params.py +++ b/src/stagehand/types/session_start_params.py @@ -175,6 +175,10 @@ class BrowserbaseSessionCreateParamsBrowserSettings(TypedDict, total=False): block_ads: Annotated[bool, PropertyInfo(alias="blockAds")] + captcha_image_selector: Annotated[str, PropertyInfo(alias="captchaImageSelector")] + + captcha_input_selector: Annotated[str, PropertyInfo(alias="captchaInputSelector")] + context: BrowserbaseSessionCreateParamsBrowserSettingsContext extension_id: Annotated[str, PropertyInfo(alias="extensionId")] @@ -183,10 +187,14 @@ class BrowserbaseSessionCreateParamsBrowserSettings(TypedDict, total=False): log_session: Annotated[bool, PropertyInfo(alias="logSession")] + os: Literal["windows", "mac", "linux", "mobile", "tablet"] + record_session: Annotated[bool, PropertyInfo(alias="recordSession")] solve_captchas: Annotated[bool, PropertyInfo(alias="solveCaptchas")] + verified: bool + viewport: BrowserbaseSessionCreateParamsBrowserSettingsViewport diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 8dfddbe2..83a69942 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -875,6 +875,8 @@ def test_method_start_with_all_params(self, client: Stagehand) -> None: "browser_settings": { "advanced_stealth": True, "block_ads": True, + "captcha_image_selector": "captchaImageSelector", + "captcha_input_selector": "captchaInputSelector", "context": { "id": "id", "persist": True, @@ -894,8 +896,10 @@ def test_method_start_with_all_params(self, client: Stagehand) -> None: }, }, "log_session": True, + "os": "windows", "record_session": True, "solve_captchas": True, + "verified": True, "viewport": { "height": 0, "width": 0, @@ -1801,6 +1805,8 @@ async def test_method_start_with_all_params(self, async_client: AsyncStagehand) "browser_settings": { "advanced_stealth": True, "block_ads": True, + "captcha_image_selector": "captchaImageSelector", + "captcha_input_selector": "captchaInputSelector", "context": { "id": "id", "persist": True, @@ -1820,8 +1826,10 @@ async def test_method_start_with_all_params(self, async_client: AsyncStagehand) }, }, "log_session": True, + "os": "windows", "record_session": True, "solve_captchas": True, + "verified": True, "viewport": { "height": 0, "width": 0, diff --git a/tests/test_client.py b/tests/test_client.py index e962cdba..37ca5a76 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -505,6 +505,30 @@ def test_default_query_option(self) -> None: client.close() + def test_hardcoded_query_params_in_url(self, client: Stagehand) -> None: + request = client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: Stagehand) -> None: request = client._build_request( FinalRequestOptions( @@ -1552,6 +1576,30 @@ async def test_default_query_option(self) -> None: await client.close() + async def test_hardcoded_query_params_in_url(self, async_client: AsyncStagehand) -> None: + request = async_client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: Stagehand) -> None: request = client._build_request( FinalRequestOptions( diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py index 0d751b46..873eaf74 100644 --- a/tests/test_extract_files.py +++ b/tests/test_extract_files.py @@ -35,6 +35,15 @@ def test_multiple_files() -> None: assert query == {"documents": [{}, {}]} +def test_top_level_file_array() -> None: + query = {"files": [b"file one", b"file two"], "title": "hello"} + assert extract_files(query, paths=[["files", ""]]) == [ + ("files[]", b"file one"), + ("files[]", b"file two"), + ] + assert query == {"title": "hello"} + + @pytest.mark.parametrize( "query,paths,expected", [