From af95b3a73cdb17795ee033314c998b4f0eb4e5c0 Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Mon, 28 Oct 2024 21:21:18 +0000 Subject: [PATCH] feat(api): update via SDK Studio --- .stats.yml | 2 +- README.md | 4 ++ requirements-dev.lock | 21 ++++------ requirements.lock | 8 ++-- src/browserbase/__init__.py | 2 + src/browserbase/_client.py | 83 +++++++++++++++++++++++++++++++------ src/browserbase/_compat.py | 2 +- src/browserbase/_models.py | 10 ++--- src/browserbase/_types.py | 6 ++- tests/conftest.py | 14 ++++--- tests/test_client.py | 20 +++++++++ 11 files changed, 129 insertions(+), 43 deletions(-) diff --git a/.stats.yml b/.stats.yml index 70bddde..e5f4ae3 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-60444f8b1aa1aa8dbec1e9f11e929c2b7ac27470764ef5f1796134fc27f3381c.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-9f93c744538f57747ea1385817e21b40c318b65ebc155dca8950268beb280bc9.yml diff --git a/README.md b/README.md index 9035de9..a170345 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ from browserbase import Browserbase client = Browserbase( # This is the default and can be omitted api_key=os.environ.get("BROWSERBASE_API_KEY"), + # or 'production' | 'local'; defaults to "production". + environment="development", ) context = client.contexts.create( @@ -58,6 +60,8 @@ from browserbase import AsyncBrowserbase client = AsyncBrowserbase( # This is the default and can be omitted api_key=os.environ.get("BROWSERBASE_API_KEY"), + # or 'production' | 'local'; defaults to "production". + environment="development", ) diff --git a/requirements-dev.lock b/requirements-dev.lock index b6be61c..20a4ebd 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -16,8 +16,6 @@ anyio==4.4.0 # via httpx argcomplete==3.1.2 # via nox -attrs==23.1.0 - # via pytest certifi==2023.7.22 # via httpcore # via httpx @@ -28,8 +26,9 @@ distlib==0.3.7 # via virtualenv distro==1.8.0 # via browserbase -exceptiongroup==1.1.3 +exceptiongroup==1.2.2 # via anyio + # via pytest filelock==3.12.4 # via virtualenv h11==0.14.0 @@ -60,20 +59,18 @@ packaging==23.2 # via pytest platformdirs==3.11.0 # via virtualenv -pluggy==1.3.0 - # via pytest -py==1.11.0 +pluggy==1.5.0 # via pytest -pydantic==2.7.1 +pydantic==2.9.2 # via browserbase -pydantic-core==2.18.2 +pydantic-core==2.23.4 # via pydantic pygments==2.18.0 # via rich pyright==1.1.380 -pytest==7.1.1 +pytest==8.3.3 # via pytest-asyncio -pytest-asyncio==0.21.1 +pytest-asyncio==0.24.0 python-dateutil==2.8.2 # via time-machine pytz==2023.3.post1 @@ -90,10 +87,10 @@ sniffio==1.3.0 # via browserbase # via httpx time-machine==2.9.0 -tomli==2.0.1 +tomli==2.0.2 # via mypy # via pytest -typing-extensions==4.8.0 +typing-extensions==4.12.2 # via anyio # via browserbase # via mypy diff --git a/requirements.lock b/requirements.lock index 44b55d0..5f5acc2 100644 --- a/requirements.lock +++ b/requirements.lock @@ -19,7 +19,7 @@ certifi==2023.7.22 # via httpx distro==1.8.0 # via browserbase -exceptiongroup==1.1.3 +exceptiongroup==1.2.2 # via anyio h11==0.14.0 # via httpcore @@ -30,15 +30,15 @@ httpx==0.25.2 idna==3.4 # via anyio # via httpx -pydantic==2.7.1 +pydantic==2.9.2 # via browserbase -pydantic-core==2.18.2 +pydantic-core==2.23.4 # via pydantic sniffio==1.3.0 # via anyio # via browserbase # via httpx -typing-extensions==4.8.0 +typing-extensions==4.12.2 # via anyio # via browserbase # via pydantic diff --git a/src/browserbase/__init__.py b/src/browserbase/__init__.py index 4b1d280..ea5a598 100644 --- a/src/browserbase/__init__.py +++ b/src/browserbase/__init__.py @@ -4,6 +4,7 @@ from ._types import NOT_GIVEN, NoneType, NotGiven, Transport, ProxiesTypes from ._utils import file_from_path from ._client import ( + ENVIRONMENTS, Client, Stream, Timeout, @@ -68,6 +69,7 @@ "AsyncStream", "Browserbase", "AsyncBrowserbase", + "ENVIRONMENTS", "file_from_path", "BaseModel", "DEFAULT_TIMEOUT", diff --git a/src/browserbase/_client.py b/src/browserbase/_client.py index df49eed..461f9da 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, Union, Mapping -from typing_extensions import Self, override +from typing import Any, Dict, Union, Mapping, cast +from typing_extensions import Self, Literal, override import httpx @@ -33,6 +33,7 @@ ) __all__ = [ + "ENVIRONMENTS", "Timeout", "Transport", "ProxiesTypes", @@ -44,6 +45,12 @@ "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 @@ -56,11 +63,14 @@ class Browserbase(SyncAPIClient): # client options api_key: str + _environment: Literal["production", "development", "local"] | NotGiven + def __init__( self, *, api_key: str | None = None, - base_url: str | httpx.URL | None = None, + environment: Literal["production", "development", "local"] | NotGiven = NOT_GIVEN, + base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN, timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -91,10 +101,31 @@ def __init__( ) self.api_key = api_key - if base_url is None: - base_url = os.environ.get("BROWSERBASE_BASE_URL") - if base_url is None: - base_url = f"https://api.dev.browserbase.com" + 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 super().__init__( version=__version__, @@ -138,6 +169,7 @@ 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, @@ -173,6 +205,7 @@ 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, @@ -230,11 +263,14 @@ class AsyncBrowserbase(AsyncAPIClient): # client options api_key: str + _environment: Literal["production", "development", "local"] | NotGiven + def __init__( self, *, api_key: str | None = None, - base_url: str | httpx.URL | None = None, + environment: Literal["production", "development", "local"] | NotGiven = NOT_GIVEN, + base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN, timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN, max_retries: int = DEFAULT_MAX_RETRIES, default_headers: Mapping[str, str] | None = None, @@ -265,10 +301,31 @@ def __init__( ) self.api_key = api_key - if base_url is None: - base_url = os.environ.get("BROWSERBASE_BASE_URL") - if base_url is None: - base_url = f"https://api.dev.browserbase.com" + 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 super().__init__( version=__version__, @@ -312,6 +369,7 @@ 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, @@ -347,6 +405,7 @@ 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/src/browserbase/_compat.py b/src/browserbase/_compat.py index 162a6fb..d89920d 100644 --- a/src/browserbase/_compat.py +++ b/src/browserbase/_compat.py @@ -133,7 +133,7 @@ def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: def model_dump( model: pydantic.BaseModel, *, - exclude: IncEx = None, + exclude: IncEx | None = None, exclude_unset: bool = False, exclude_defaults: bool = False, warnings: bool = True, diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index d386eaa..42551b7 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -176,7 +176,7 @@ def __str__(self) -> str: # Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836. @classmethod @override - def construct( + def construct( # pyright: ignore[reportIncompatibleMethodOverride] cls: Type[ModelT], _fields_set: set[str] | None = None, **values: object, @@ -248,8 +248,8 @@ def model_dump( self, *, mode: Literal["json", "python"] | str = "python", - include: IncEx = None, - exclude: IncEx = None, + include: IncEx | None = None, + exclude: IncEx | None = None, by_alias: bool = False, exclude_unset: bool = False, exclude_defaults: bool = False, @@ -303,8 +303,8 @@ def model_dump_json( self, *, indent: int | None = None, - include: IncEx = None, - exclude: IncEx = None, + include: IncEx | None = None, + exclude: IncEx | None = None, by_alias: bool = False, exclude_unset: bool = False, exclude_defaults: bool = False, diff --git a/src/browserbase/_types.py b/src/browserbase/_types.py index 5ab6985..1691090 100644 --- a/src/browserbase/_types.py +++ b/src/browserbase/_types.py @@ -16,7 +16,7 @@ Optional, Sequence, ) -from typing_extensions import Literal, Protocol, TypeAlias, TypedDict, override, runtime_checkable +from typing_extensions import Set, Literal, Protocol, TypeAlias, TypedDict, override, runtime_checkable import httpx import pydantic @@ -193,7 +193,9 @@ def get(self, __key: str) -> str | None: ... # Note: copied from Pydantic # https://github.com/pydantic/pydantic/blob/32ea570bf96e84234d2992e1ddf40ab8a565925a/pydantic/main.py#L49 -IncEx: TypeAlias = "set[int] | set[str] | dict[int, Any] | dict[str, Any] | None" +IncEx: TypeAlias = Union[ + Set[int], Set[str], Mapping[int, Union["IncEx", Literal[True]]], Mapping[str, Union["IncEx", Literal[True]]] +] PostParser = Callable[[Any], Any] diff --git a/tests/conftest.py b/tests/conftest.py index 06a9e89..15ddbca 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,11 @@ from __future__ import annotations import os -import asyncio import logging from typing import TYPE_CHECKING, Iterator, AsyncIterator import pytest +from pytest_asyncio import is_async_test from browserbase import Browserbase, AsyncBrowserbase @@ -17,11 +17,13 @@ logging.getLogger("browserbase").setLevel(logging.DEBUG) -@pytest.fixture(scope="session") -def event_loop() -> Iterator[asyncio.AbstractEventLoop]: - loop = asyncio.new_event_loop() - yield loop - loop.close() +# automatically add `pytest.mark.asyncio()` to all of our async tests +# so we don't have to add that boilerplate everywhere +def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: + pytest_asyncio_tests = (item for item in items if is_async_test(item)) + session_scope_marker = pytest.mark.asyncio(loop_scope="session") + for async_test in pytest_asyncio_tests: + async_test.add_marker(session_scope_marker, append=False) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") diff --git a/tests/test_client.py b/tests/test_client.py index fe8d7f8..9fe3b90 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -556,6 +556,16 @@ 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", [ @@ -1332,6 +1342,16 @@ 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", [