diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e8285b71..4f9005ea 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.5" + ".": "0.1.0-alpha.6" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 70bdddeb..e5f4ae33 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/CHANGELOG.md b/CHANGELOG.md index 8cb679ff..c691761d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 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) + +### Features + +* **api:** update via SDK Studio ([#32](https://github.com/browserbase/sdk-python/issues/32)) ([a956d94](https://github.com/browserbase/sdk-python/commit/a956d9415cadd959c1a40a86d64117ffe5812e48)) + ## 0.1.0-alpha.5 (2024-10-28) Full Changelog: [v0.1.0-alpha.4...v0.1.0-alpha.5](https://github.com/browserbase/sdk-python/compare/v0.1.0-alpha.4...v0.1.0-alpha.5) diff --git a/README.md b/README.md index 62726e0f..9971503f 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ 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: diff --git a/pyproject.toml b/pyproject.toml index 052598c4..42f9c51d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "0.1.0-alpha.5" +version = "0.1.0-alpha.6" description = "The official Python library for the browserbase API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/requirements-dev.lock b/requirements-dev.lock index 0be40775..83ce0203 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -4,121 +4,115 @@ # last locked with the following flags: # pre: false # features: [] -# all-features: true +# all-features: false # with-sources: false # generate-hashes: false # universal: false -e file:. -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.6.2.post1 # via browserbase # via httpx -argcomplete==3.1.2 +argcomplete==3.5.1 # via nox -attrs==23.1.0 +attrs==24.2.0 # via outcome - # via pytest # via trio -certifi==2023.7.22 +certifi==2024.8.30 # via httpcore # via httpx # via requests # via selenium charset-normalizer==3.4.0 # via requests -colorlog==6.7.0 +colorlog==6.8.2 # via nox -dirty-equals==0.6.0 -distlib==0.3.7 +dirty-equals==0.8.0 +distlib==0.3.9 # via virtualenv -distro==1.8.0 +distro==1.9.0 # via browserbase -exceptiongroup==1.1.3 +exceptiongroup==1.2.2 # via anyio + # via pytest # via trio # via trio-websocket -filelock==3.12.4 +filelock==3.16.1 # via virtualenv greenlet==3.1.1 # via playwright h11==0.14.0 # via httpcore # via wsproto -httpcore==1.0.2 +httpcore==1.0.6 # via httpx -httpx==0.25.2 +httpx==0.27.2 # via browserbase # via respx -idna==3.4 +idna==3.10 # via anyio # via httpx # via requests # via trio -importlib-metadata==7.0.0 +importlib-metadata==8.5.0 iniconfig==2.0.0 # via pytest markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -mypy==1.11.2 +mypy==1.13.0 mypy-extensions==1.0.0 # via mypy -nodeenv==1.8.0 +nodeenv==1.9.1 # via pyright -nox==2023.4.22 +nox==2024.10.9 outcome==1.3.0.post0 # via trio -packaging==23.2 +packaging==24.1 # via nox # via pytest -platformdirs==3.11.0 +platformdirs==4.3.6 # via virtualenv playwright==1.48.0 # via pytest-playwright -pluggy==1.3.0 +pluggy==1.5.0 # via pytest -py==1.11.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 pyee==12.0.0 # via playwright pygments==2.18.0 # via rich -pyright==1.1.380 +pyright==1.1.386 pysocks==1.7.1 # via urllib3 -pytest==7.1.1 +pytest==8.3.3 # via pytest-asyncio # via pytest-base-url # via pytest-playwright -pytest-asyncio==0.21.1 +pytest-asyncio==0.24.0 pytest-base-url==2.1.0 # via pytest-playwright pytest-playwright==0.5.2 -python-dateutil==2.8.2 +python-dateutil==2.9.0.post0 # via time-machine python-dotenv==1.0.1 python-slugify==8.0.4 # via pytest-playwright -pytz==2023.3.post1 - # via dirty-equals requests==2.32.3 # via pytest-base-url -respx==0.20.2 -rich==13.7.1 -ruff==0.6.9 -selenium==4.16.0 -setuptools==68.2.2 - # via nodeenv +respx==0.21.1 +rich==13.9.3 +ruff==0.7.1 +selenium==4.25.0 six==1.16.0 # via python-dateutil -sniffio==1.3.0 +sniffio==1.3.1 # via anyio # via browserbase # via httpx @@ -127,28 +121,34 @@ sortedcontainers==2.4.0 # via trio text-unidecode==1.3 # via python-slugify -time-machine==2.9.0 -tomli==2.0.1 +time-machine==2.16.0 +tomli==2.0.2 # via mypy + # via nox # via pytest -trio==0.24.0 +trio==0.27.0 # via selenium # via trio-websocket trio-websocket==0.11.1 # via selenium -typing-extensions==4.8.0 +typing-extensions==4.12.2 # via anyio # via browserbase # via mypy # via pydantic # via pydantic-core # via pyee + # via pyright + # via rich + # via selenium urllib3==2.2.3 # via requests # via selenium -virtualenv==20.24.5 +virtualenv==20.27.1 # via nox +websocket-client==1.8.0 + # via selenium wsproto==1.2.0 # via trio-websocket -zipp==3.17.0 +zipp==3.20.2 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock index b71b55a9..4bff3dd0 100644 --- a/requirements.lock +++ b/requirements.lock @@ -20,7 +20,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 @@ -31,15 +31,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 4b1d2804..ea5a5985 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 df49eed2..461f9dae 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 162a6fbe..d89920d9 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 d386eaa3..42551b76 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 5ab69853..1691090d 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/src/browserbase/_version.py b/src/browserbase/_version.py index 1dd5a80e..47f88c6c 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.5" # x-release-please-version +__version__ = "0.1.0-alpha.6" # x-release-please-version diff --git a/tests/conftest.py b/tests/conftest.py index 06a9e893..15ddbcad 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 fe8d7f86..9fe3b908 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", [