From c69466218e105b524b4ad98208283b96eccf056d Mon Sep 17 00:00:00 2001 From: Stainless Bot Date: Mon, 28 Oct 2024 23:29:04 +0000 Subject: [PATCH 1/2] feat(api): update via SDK Studio (#35) --- README.md | 42 ++++++++++-------- SECURITY.md | 2 +- pyproject.toml | 4 +- src/browserbase/__init__.py | 2 - src/browserbase/_client.py | 87 ++++++------------------------------- tests/test_client.py | 68 ++++++++++------------------- 6 files changed, 64 insertions(+), 141 deletions(-) diff --git a/README.md b/README.md index 9971503..72bc5a3 100644 --- a/README.md +++ b/README.md @@ -37,14 +37,15 @@ BROWSERBASE_PROJECT_ID = os.environ.get("BROWSERBASE_PROJECT_ID") bb = Browserbase( # This is the default and can be omitted api_key=BROWSERBASE_API_KEY, - # or 'production' | 'local'; defaults to "production". - environment="development", ) -def run(playwright: Playwright) -> None: - # Create a session on Browserbase - session = bb.sessions.create(project_id=BROWSERBASE_PROJECT_ID) +session = client.sessions.create( + project_id=BROWSERBASE_PROJECT_ID, + proxies=True, +) +print(session.id) +def run(playwright: Playwright) -> None: # Connect to the remote session chromium = playwright.chromium browser = chromium.connect_over_cdp(session.connect_url) @@ -67,9 +68,7 @@ def run(playwright: Playwright) -> None: if __name__ == "__main__": with sync_playwright() as playwright: run(playwright) - ``` - ## Examples See the [examples](examples) directory for more usage examples. @@ -112,8 +111,9 @@ from browserbase import Browserbase client = Browserbase() try: - client.contexts.create( - project_id="projectId", + client.sessions.create( + project_id="your_project_id", + proxies=True, ) except browserbase.APIConnectionError as e: print("The server could not be reached") @@ -157,8 +157,9 @@ client = Browserbase( ) # Or, configure per-request: -client.with_options(max_retries=5).contexts.create( - project_id="projectId", +client.with_options(max_retries=5).sessions.create( + project_id="your_project_id", + proxies=True, ) ``` @@ -182,8 +183,9 @@ client = Browserbase( ) # Override per-request: -client.with_options(timeout=5.0).contexts.create( - project_id="projectId", +client.with_options(timeout=5.0).sessions.create( + project_id="your_project_id", + proxies=True, ) ``` @@ -223,13 +225,14 @@ The "raw" Response object can be accessed by prefixing `.with_raw_response.` to from browserbase import Browserbase client = Browserbase() -response = client.contexts.with_raw_response.create( - project_id="projectId", +response = client.sessions.with_raw_response.create( + project_id="your_project_id", + proxies=True, ) print(response.headers.get('X-My-Header')) -context = response.parse() # get the object that `contexts.create()` would have returned -print(context.id) +session = response.parse() # get the object that `sessions.create()` would have returned +print(session.id) ``` These methods return an [`APIResponse`](https://github.com/browserbase/sdk-python/tree/main/src/browserbase/_response.py) object. @@ -243,8 +246,9 @@ The above interface eagerly reads the full response body when you make the reque To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. ```python -with client.contexts.with_streaming_response.create( - project_id="projectId", +with client.sessions.with_streaming_response.create( + project_id="your_project_id", + proxies=True, ) as response: print(response.headers.get("X-My-Header")) diff --git a/SECURITY.md b/SECURITY.md index 06c5b6e..4fdede8 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -20,7 +20,7 @@ or products provided by Browserbase please follow the respective company's secur ### Browserbase Terms and Policies -Please contact dev-feedback@browserbase.com for any questions or concerns regarding security of our services. +Please contact support@browserbase.com for any questions or concerns regarding security of our services. --- diff --git a/pyproject.toml b/pyproject.toml index 42f9c51..56bfa29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [project] name = "browserbase" version = "0.1.0-alpha.6" -description = "The official Python library for the browserbase API" +description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" authors = [ -{ name = "Browserbase", email = "dev-feedback@browserbase.com" }, +{ name = "Browserbase", email = "support@browserbase.com" }, ] dependencies = [ "httpx>=0.23.0, <1", diff --git a/src/browserbase/__init__.py b/src/browserbase/__init__.py index ea5a598..4b1d280 100644 --- a/src/browserbase/__init__.py +++ b/src/browserbase/__init__.py @@ -4,7 +4,6 @@ from ._types import NOT_GIVEN, NoneType, NotGiven, Transport, ProxiesTypes from ._utils import file_from_path from ._client import ( - ENVIRONMENTS, Client, Stream, Timeout, @@ -69,7 +68,6 @@ "AsyncStream", "Browserbase", "AsyncBrowserbase", - "ENVIRONMENTS", "file_from_path", "BaseModel", "DEFAULT_TIMEOUT", diff --git a/src/browserbase/_client.py b/src/browserbase/_client.py index 461f9da..b3ed32f 100644 --- a/src/browserbase/_client.py +++ b/src/browserbase/_client.py @@ -3,8 +3,8 @@ from __future__ import annotations import os -from typing import Any, Dict, Union, Mapping, cast -from typing_extensions import Self, Literal, override +from typing import Any, Union, Mapping +from typing_extensions import Self, override import httpx @@ -33,7 +33,6 @@ ) __all__ = [ - "ENVIRONMENTS", "Timeout", "Transport", "ProxiesTypes", @@ -45,12 +44,6 @@ "AsyncClient", ] -ENVIRONMENTS: Dict[str, str] = { - "production": "https://api.browserbase.com", - "development": "https://api.dev.browserbase.com", - "local": "http://api.localhost", -} - class Browserbase(SyncAPIClient): contexts: resources.ContextsResource @@ -63,14 +56,11 @@ class Browserbase(SyncAPIClient): # client options api_key: str - _environment: Literal["production", "development", "local"] | NotGiven - def __init__( self, *, api_key: str | None = None, - environment: Literal["production", "development", "local"] | NotGiven = NOT_GIVEN, - base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN, + base_url: str | httpx.URL | None = None, timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -89,7 +79,7 @@ def __init__( # part of our public interface in the future. _strict_response_validation: bool = False, ) -> None: - """Construct a new synchronous browserbase client instance. + """Construct a new synchronous Browserbase client instance. This automatically infers the `api_key` argument from the `BROWSERBASE_API_KEY` environment variable if it is not provided. """ @@ -101,31 +91,10 @@ def __init__( ) self.api_key = api_key - self._environment = environment - - base_url_env = os.environ.get("BROWSERBASE_BASE_URL") - if is_given(base_url) and base_url is not None: - # cast required because mypy doesn't understand the type narrowing - base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] - elif is_given(environment): - if base_url_env and base_url is not None: - raise ValueError( - "Ambiguous URL; The `BROWSERBASE_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", - ) - - try: - base_url = ENVIRONMENTS[environment] - except KeyError as exc: - raise ValueError(f"Unknown environment: {environment}") from exc - elif base_url_env is not None: - base_url = base_url_env - else: - self._environment = environment = "production" - - try: - base_url = ENVIRONMENTS[environment] - except KeyError as exc: - raise ValueError(f"Unknown environment: {environment}") from exc + if base_url is None: + base_url = os.environ.get("BROWSERBASE_BASE_URL") + if base_url is None: + base_url = f"https://api.browserbase.com" super().__init__( version=__version__, @@ -169,7 +138,6 @@ def copy( self, *, api_key: str | None = None, - environment: Literal["production", "development", "local"] | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, http_client: httpx.Client | None = None, @@ -205,7 +173,6 @@ def copy( return self.__class__( api_key=api_key or self.api_key, base_url=base_url or self.base_url, - environment=environment or self._environment, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, max_retries=max_retries if is_given(max_retries) else self.max_retries, @@ -263,14 +230,11 @@ class AsyncBrowserbase(AsyncAPIClient): # client options api_key: str - _environment: Literal["production", "development", "local"] | NotGiven - def __init__( self, *, api_key: str | None = None, - environment: Literal["production", "development", "local"] | NotGiven = NOT_GIVEN, - base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN, + base_url: str | httpx.URL | None = None, timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -289,7 +253,7 @@ def __init__( # part of our public interface in the future. _strict_response_validation: bool = False, ) -> None: - """Construct a new async browserbase client instance. + """Construct a new async Browserbase client instance. This automatically infers the `api_key` argument from the `BROWSERBASE_API_KEY` environment variable if it is not provided. """ @@ -301,31 +265,10 @@ def __init__( ) self.api_key = api_key - self._environment = environment - - base_url_env = os.environ.get("BROWSERBASE_BASE_URL") - if is_given(base_url) and base_url is not None: - # cast required because mypy doesn't understand the type narrowing - base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] - elif is_given(environment): - if base_url_env and base_url is not None: - raise ValueError( - "Ambiguous URL; The `BROWSERBASE_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", - ) - - try: - base_url = ENVIRONMENTS[environment] - except KeyError as exc: - raise ValueError(f"Unknown environment: {environment}") from exc - elif base_url_env is not None: - base_url = base_url_env - else: - self._environment = environment = "production" - - try: - base_url = ENVIRONMENTS[environment] - except KeyError as exc: - raise ValueError(f"Unknown environment: {environment}") from exc + if base_url is None: + base_url = os.environ.get("BROWSERBASE_BASE_URL") + if base_url is None: + base_url = f"https://api.browserbase.com" super().__init__( version=__version__, @@ -369,7 +312,6 @@ def copy( self, *, api_key: str | None = None, - environment: Literal["production", "development", "local"] | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, http_client: httpx.AsyncClient | None = None, @@ -405,7 +347,6 @@ def copy( return self.__class__( api_key=api_key or self.api_key, base_url=base_url or self.base_url, - environment=environment or self._environment, timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, http_client=http_client, max_retries=max_retries if is_given(max_retries) else self.max_retries, diff --git a/tests/test_client.py b/tests/test_client.py index 9fe3b90..9679e17 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -556,16 +556,6 @@ def test_base_url_env(self) -> None: client = Browserbase(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" - # explicit environment arg requires explicitness - with update_env(BROWSERBASE_BASE_URL="http://localhost:5000/from/env"): - with pytest.raises(ValueError, match=r"you must pass base_url=None"): - Browserbase(api_key=api_key, _strict_response_validation=True, environment="production") - - client = Browserbase( - base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" - ) - assert str(client.base_url).startswith("https://api.browserbase.com") - @pytest.mark.parametrize( "client", [ @@ -728,12 +718,12 @@ def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str @mock.patch("browserbase._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: - respx_mock.post("/v1/contexts").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.post("/v1/sessions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): self.client.post( - "/v1/contexts", - body=cast(object, dict(project_id="projectId")), + "/v1/sessions", + body=cast(object, dict(project_id="your_project_id", proxies=True)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -743,12 +733,12 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No @mock.patch("browserbase._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: - respx_mock.post("/v1/contexts").mock(return_value=httpx.Response(500)) + respx_mock.post("/v1/sessions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): self.client.post( - "/v1/contexts", - body=cast(object, dict(project_id="projectId")), + "/v1/sessions", + body=cast(object, dict(project_id="your_project_id", proxies=True)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -779,9 +769,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v1/contexts").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = client.contexts.with_raw_response.create(project_id="projectId") + response = client.sessions.with_raw_response.create(project_id="projectId") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -803,9 +793,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v1/contexts").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = client.contexts.with_raw_response.create( + response = client.sessions.with_raw_response.create( project_id="projectId", extra_headers={"x-stainless-retry-count": Omit()} ) @@ -828,9 +818,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v1/contexts").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = client.contexts.with_raw_response.create( + response = client.sessions.with_raw_response.create( project_id="projectId", extra_headers={"x-stainless-retry-count": "42"} ) @@ -1342,16 +1332,6 @@ def test_base_url_env(self) -> None: client = AsyncBrowserbase(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" - # explicit environment arg requires explicitness - with update_env(BROWSERBASE_BASE_URL="http://localhost:5000/from/env"): - with pytest.raises(ValueError, match=r"you must pass base_url=None"): - AsyncBrowserbase(api_key=api_key, _strict_response_validation=True, environment="production") - - client = AsyncBrowserbase( - base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" - ) - assert str(client.base_url).startswith("https://api.browserbase.com") - @pytest.mark.parametrize( "client", [ @@ -1518,12 +1498,12 @@ async def test_parse_retry_after_header(self, remaining_retries: int, retry_afte @mock.patch("browserbase._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: - respx_mock.post("/v1/contexts").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.post("/v1/sessions").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): await self.client.post( - "/v1/contexts", - body=cast(object, dict(project_id="projectId")), + "/v1/sessions", + body=cast(object, dict(project_id="your_project_id", proxies=True)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1533,12 +1513,12 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) @mock.patch("browserbase._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None: - respx_mock.post("/v1/contexts").mock(return_value=httpx.Response(500)) + respx_mock.post("/v1/sessions").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): await self.client.post( - "/v1/contexts", - body=cast(object, dict(project_id="projectId")), + "/v1/sessions", + body=cast(object, dict(project_id="your_project_id", proxies=True)), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1570,9 +1550,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v1/contexts").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = await client.contexts.with_raw_response.create(project_id="projectId") + response = await client.sessions.with_raw_response.create(project_id="projectId") assert response.retries_taken == failures_before_success assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success @@ -1595,9 +1575,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v1/contexts").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = await client.contexts.with_raw_response.create( + response = await client.sessions.with_raw_response.create( project_id="projectId", extra_headers={"x-stainless-retry-count": Omit()} ) @@ -1621,9 +1601,9 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.post("/v1/contexts").mock(side_effect=retry_handler) + respx_mock.post("/v1/sessions").mock(side_effect=retry_handler) - response = await client.contexts.with_raw_response.create( + response = await client.sessions.with_raw_response.create( project_id="projectId", extra_headers={"x-stainless-retry-count": "42"} ) From 8e2acb107c4b3b520626cee0a83907957b35834e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 23:29:17 +0000 Subject: [PATCH 2/2] release: 0.1.0-alpha.7 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 8 ++++++++ pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4f9005e..b5db7ce 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.6" + ".": "0.1.0-alpha.7" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c691761..3f53046 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.1.0-alpha.7 (2024-10-28) + +Full Changelog: [v0.1.0-alpha.6...v0.1.0-alpha.7](https://github.com/browserbase/sdk-python/compare/v0.1.0-alpha.6...v0.1.0-alpha.7) + +### Features + +* **api:** update via SDK Studio ([#35](https://github.com/browserbase/sdk-python/issues/35)) ([c694662](https://github.com/browserbase/sdk-python/commit/c69466218e105b524b4ad98208283b96eccf056d)) + ## 0.1.0-alpha.6 (2024-10-28) Full Changelog: [v0.1.0-alpha.5...v0.1.0-alpha.6](https://github.com/browserbase/sdk-python/compare/v0.1.0-alpha.5...v0.1.0-alpha.6) diff --git a/pyproject.toml b/pyproject.toml index 56bfa29..0ed3bd7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "0.1.0-alpha.6" +version = "0.1.0-alpha.7" description = "The official Python library for the Browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/browserbase/_version.py b/src/browserbase/_version.py index 47f88c6..382ba40 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__ = "0.1.0-alpha.6" # x-release-please-version +__version__ = "0.1.0-alpha.7" # x-release-please-version