Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit a956d94

Browse files
author
Stainless Bot
committedOct 28, 2024·
feat(api): update via SDK Studio (#32)
1 parent 50b9017 commit a956d94

11 files changed

+164
-77
lines changed
 

‎.stats.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
configured_endpoints: 18
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-60444f8b1aa1aa8dbec1e9f11e929c2b7ac27470764ef5f1796134fc27f3381c.yml
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-9f93c744538f57747ea1385817e21b40c318b65ebc155dca8950268beb280bc9.yml

‎README.md

+2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ BROWSERBASE_PROJECT_ID = os.environ.get("BROWSERBASE_PROJECT_ID")
3737
bb = Browserbase(
3838
# This is the default and can be omitted
3939
api_key=BROWSERBASE_API_KEY,
40+
# or 'production' | 'local'; defaults to "production".
41+
environment="development",
4042
)
4143

4244
def run(playwright: Playwright) -> None:

‎requirements-dev.lock

+46-46
Original file line numberDiff line numberDiff line change
@@ -4,121 +4,115 @@
44
# last locked with the following flags:
55
# pre: false
66
# features: []
7-
# all-features: true
7+
# all-features: false
88
# with-sources: false
99
# generate-hashes: false
1010
# universal: false
1111

1212
-e file:.
13-
annotated-types==0.6.0
13+
annotated-types==0.7.0
1414
# via pydantic
15-
anyio==4.4.0
15+
anyio==4.6.2.post1
1616
# via browserbase
1717
# via httpx
18-
argcomplete==3.1.2
18+
argcomplete==3.5.1
1919
# via nox
20-
attrs==23.1.0
20+
attrs==24.2.0
2121
# via outcome
22-
# via pytest
2322
# via trio
24-
certifi==2023.7.22
23+
certifi==2024.8.30
2524
# via httpcore
2625
# via httpx
2726
# via requests
2827
# via selenium
2928
charset-normalizer==3.4.0
3029
# via requests
31-
colorlog==6.7.0
30+
colorlog==6.8.2
3231
# via nox
33-
dirty-equals==0.6.0
34-
distlib==0.3.7
32+
dirty-equals==0.8.0
33+
distlib==0.3.9
3534
# via virtualenv
36-
distro==1.8.0
35+
distro==1.9.0
3736
# via browserbase
38-
exceptiongroup==1.1.3
37+
exceptiongroup==1.2.2
3938
# via anyio
39+
# via pytest
4040
# via trio
4141
# via trio-websocket
42-
filelock==3.12.4
42+
filelock==3.16.1
4343
# via virtualenv
4444
greenlet==3.1.1
4545
# via playwright
4646
h11==0.14.0
4747
# via httpcore
4848
# via wsproto
49-
httpcore==1.0.2
49+
httpcore==1.0.6
5050
# via httpx
51-
httpx==0.25.2
51+
httpx==0.27.2
5252
# via browserbase
5353
# via respx
54-
idna==3.4
54+
idna==3.10
5555
# via anyio
5656
# via httpx
5757
# via requests
5858
# via trio
59-
importlib-metadata==7.0.0
59+
importlib-metadata==8.5.0
6060
iniconfig==2.0.0
6161
# via pytest
6262
markdown-it-py==3.0.0
6363
# via rich
6464
mdurl==0.1.2
6565
# via markdown-it-py
66-
mypy==1.11.2
66+
mypy==1.13.0
6767
mypy-extensions==1.0.0
6868
# via mypy
69-
nodeenv==1.8.0
69+
nodeenv==1.9.1
7070
# via pyright
71-
nox==2023.4.22
71+
nox==2024.10.9
7272
outcome==1.3.0.post0
7373
# via trio
74-
packaging==23.2
74+
packaging==24.1
7575
# via nox
7676
# via pytest
77-
platformdirs==3.11.0
77+
platformdirs==4.3.6
7878
# via virtualenv
7979
playwright==1.48.0
8080
# via pytest-playwright
81-
pluggy==1.3.0
81+
pluggy==1.5.0
8282
# via pytest
83-
py==1.11.0
84-
# via pytest
85-
pydantic==2.7.1
83+
pydantic==2.9.2
8684
# via browserbase
87-
pydantic-core==2.18.2
85+
pydantic-core==2.23.4
8886
# via pydantic
8987
pyee==12.0.0
9088
# via playwright
9189
pygments==2.18.0
9290
# via rich
93-
pyright==1.1.380
91+
pyright==1.1.386
9492
pysocks==1.7.1
9593
# via urllib3
96-
pytest==7.1.1
94+
pytest==8.3.3
9795
# via pytest-asyncio
9896
# via pytest-base-url
9997
# via pytest-playwright
100-
pytest-asyncio==0.21.1
98+
pytest-asyncio==0.24.0
10199
pytest-base-url==2.1.0
102100
# via pytest-playwright
103101
pytest-playwright==0.5.2
104-
python-dateutil==2.8.2
102+
python-dateutil==2.9.0.post0
105103
# via time-machine
106104
python-dotenv==1.0.1
107105
python-slugify==8.0.4
108106
# via pytest-playwright
109-
pytz==2023.3.post1
110-
# via dirty-equals
111107
requests==2.32.3
112108
# via pytest-base-url
113-
respx==0.20.2
114-
rich==13.7.1
115-
ruff==0.6.9
116-
selenium==4.16.0
117-
setuptools==68.2.2
118-
# via nodeenv
109+
respx==0.21.1
110+
rich==13.9.3
111+
ruff==0.7.1
112+
selenium==4.25.0
119113
six==1.16.0
120114
# via python-dateutil
121-
sniffio==1.3.0
115+
sniffio==1.3.1
122116
# via anyio
123117
# via browserbase
124118
# via httpx
@@ -127,28 +121,34 @@ sortedcontainers==2.4.0
127121
# via trio
128122
text-unidecode==1.3
129123
# via python-slugify
130-
time-machine==2.9.0
131-
tomli==2.0.1
124+
time-machine==2.16.0
125+
tomli==2.0.2
132126
# via mypy
127+
# via nox
133128
# via pytest
134-
trio==0.24.0
129+
trio==0.27.0
135130
# via selenium
136131
# via trio-websocket
137132
trio-websocket==0.11.1
138133
# via selenium
139-
typing-extensions==4.8.0
134+
typing-extensions==4.12.2
140135
# via anyio
141136
# via browserbase
142137
# via mypy
143138
# via pydantic
144139
# via pydantic-core
145140
# via pyee
141+
# via pyright
142+
# via rich
143+
# via selenium
146144
urllib3==2.2.3
147145
# via requests
148146
# via selenium
149-
virtualenv==20.24.5
147+
virtualenv==20.27.1
150148
# via nox
149+
websocket-client==1.8.0
150+
# via selenium
151151
wsproto==1.2.0
152152
# via trio-websocket
153-
zipp==3.17.0
153+
zipp==3.20.2
154154
# via importlib-metadata

‎requirements.lock

+4-4
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ certifi==2023.7.22
2020
# via httpx
2121
distro==1.8.0
2222
# via browserbase
23-
exceptiongroup==1.1.3
23+
exceptiongroup==1.2.2
2424
# via anyio
2525
h11==0.14.0
2626
# via httpcore
@@ -31,15 +31,15 @@ httpx==0.25.2
3131
idna==3.4
3232
# via anyio
3333
# via httpx
34-
pydantic==2.7.1
34+
pydantic==2.9.2
3535
# via browserbase
36-
pydantic-core==2.18.2
36+
pydantic-core==2.23.4
3737
# via pydantic
3838
sniffio==1.3.0
3939
# via anyio
4040
# via browserbase
4141
# via httpx
42-
typing-extensions==4.8.0
42+
typing-extensions==4.12.2
4343
# via anyio
4444
# via browserbase
4545
# via pydantic

‎src/browserbase/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from ._types import NOT_GIVEN, NoneType, NotGiven, Transport, ProxiesTypes
55
from ._utils import file_from_path
66
from ._client import (
7+
ENVIRONMENTS,
78
Client,
89
Stream,
910
Timeout,
@@ -68,6 +69,7 @@
6869
"AsyncStream",
6970
"Browserbase",
7071
"AsyncBrowserbase",
72+
"ENVIRONMENTS",
7173
"file_from_path",
7274
"BaseModel",
7375
"DEFAULT_TIMEOUT",

‎src/browserbase/_client.py

+71-12
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from __future__ import annotations
44

55
import os
6-
from typing import Any, Union, Mapping
7-
from typing_extensions import Self, override
6+
from typing import Any, Dict, Union, Mapping, cast
7+
from typing_extensions import Self, Literal, override
88

99
import httpx
1010

@@ -33,6 +33,7 @@
3333
)
3434

3535
__all__ = [
36+
"ENVIRONMENTS",
3637
"Timeout",
3738
"Transport",
3839
"ProxiesTypes",
@@ -44,6 +45,12 @@
4445
"AsyncClient",
4546
]
4647

48+
ENVIRONMENTS: Dict[str, str] = {
49+
"production": "https://api.browserbase.com",
50+
"development": "https://api.dev.browserbase.com",
51+
"local": "http://api.localhost",
52+
}
53+
4754

4855
class Browserbase(SyncAPIClient):
4956
contexts: resources.ContextsResource
@@ -56,11 +63,14 @@ class Browserbase(SyncAPIClient):
5663
# client options
5764
api_key: str
5865

66+
_environment: Literal["production", "development", "local"] | NotGiven
67+
5968
def __init__(
6069
self,
6170
*,
6271
api_key: str | None = None,
63-
base_url: str | httpx.URL | None = None,
72+
environment: Literal["production", "development", "local"] | NotGiven = NOT_GIVEN,
73+
base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN,
6474
timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN,
6575
max_retries: int = DEFAULT_MAX_RETRIES,
6676
default_headers: Mapping[str, str] | None = None,
@@ -91,10 +101,31 @@ def __init__(
91101
)
92102
self.api_key = api_key
93103

94-
if base_url is None:
95-
base_url = os.environ.get("BROWSERBASE_BASE_URL")
96-
if base_url is None:
97-
base_url = f"https://api.dev.browserbase.com"
104+
self._environment = environment
105+
106+
base_url_env = os.environ.get("BROWSERBASE_BASE_URL")
107+
if is_given(base_url) and base_url is not None:
108+
# cast required because mypy doesn't understand the type narrowing
109+
base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast]
110+
elif is_given(environment):
111+
if base_url_env and base_url is not None:
112+
raise ValueError(
113+
"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",
114+
)
115+
116+
try:
117+
base_url = ENVIRONMENTS[environment]
118+
except KeyError as exc:
119+
raise ValueError(f"Unknown environment: {environment}") from exc
120+
elif base_url_env is not None:
121+
base_url = base_url_env
122+
else:
123+
self._environment = environment = "production"
124+
125+
try:
126+
base_url = ENVIRONMENTS[environment]
127+
except KeyError as exc:
128+
raise ValueError(f"Unknown environment: {environment}") from exc
98129

99130
super().__init__(
100131
version=__version__,
@@ -138,6 +169,7 @@ def copy(
138169
self,
139170
*,
140171
api_key: str | None = None,
172+
environment: Literal["production", "development", "local"] | None = None,
141173
base_url: str | httpx.URL | None = None,
142174
timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
143175
http_client: httpx.Client | None = None,
@@ -173,6 +205,7 @@ def copy(
173205
return self.__class__(
174206
api_key=api_key or self.api_key,
175207
base_url=base_url or self.base_url,
208+
environment=environment or self._environment,
176209
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
177210
http_client=http_client,
178211
max_retries=max_retries if is_given(max_retries) else self.max_retries,
@@ -230,11 +263,14 @@ class AsyncBrowserbase(AsyncAPIClient):
230263
# client options
231264
api_key: str
232265

266+
_environment: Literal["production", "development", "local"] | NotGiven
267+
233268
def __init__(
234269
self,
235270
*,
236271
api_key: str | None = None,
237-
base_url: str | httpx.URL | None = None,
272+
environment: Literal["production", "development", "local"] | NotGiven = NOT_GIVEN,
273+
base_url: str | httpx.URL | None | NotGiven = NOT_GIVEN,
238274
timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN,
239275
max_retries: int = DEFAULT_MAX_RETRIES,
240276
default_headers: Mapping[str, str] | None = None,
@@ -265,10 +301,31 @@ def __init__(
265301
)
266302
self.api_key = api_key
267303

268-
if base_url is None:
269-
base_url = os.environ.get("BROWSERBASE_BASE_URL")
270-
if base_url is None:
271-
base_url = f"https://api.dev.browserbase.com"
304+
self._environment = environment
305+
306+
base_url_env = os.environ.get("BROWSERBASE_BASE_URL")
307+
if is_given(base_url) and base_url is not None:
308+
# cast required because mypy doesn't understand the type narrowing
309+
base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast]
310+
elif is_given(environment):
311+
if base_url_env and base_url is not None:
312+
raise ValueError(
313+
"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",
314+
)
315+
316+
try:
317+
base_url = ENVIRONMENTS[environment]
318+
except KeyError as exc:
319+
raise ValueError(f"Unknown environment: {environment}") from exc
320+
elif base_url_env is not None:
321+
base_url = base_url_env
322+
else:
323+
self._environment = environment = "production"
324+
325+
try:
326+
base_url = ENVIRONMENTS[environment]
327+
except KeyError as exc:
328+
raise ValueError(f"Unknown environment: {environment}") from exc
272329

273330
super().__init__(
274331
version=__version__,
@@ -312,6 +369,7 @@ def copy(
312369
self,
313370
*,
314371
api_key: str | None = None,
372+
environment: Literal["production", "development", "local"] | None = None,
315373
base_url: str | httpx.URL | None = None,
316374
timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
317375
http_client: httpx.AsyncClient | None = None,
@@ -347,6 +405,7 @@ def copy(
347405
return self.__class__(
348406
api_key=api_key or self.api_key,
349407
base_url=base_url or self.base_url,
408+
environment=environment or self._environment,
350409
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
351410
http_client=http_client,
352411
max_retries=max_retries if is_given(max_retries) else self.max_retries,

‎src/browserbase/_compat.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str:
133133
def model_dump(
134134
model: pydantic.BaseModel,
135135
*,
136-
exclude: IncEx = None,
136+
exclude: IncEx | None = None,
137137
exclude_unset: bool = False,
138138
exclude_defaults: bool = False,
139139
warnings: bool = True,

‎src/browserbase/_models.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ def __str__(self) -> str:
176176
# Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836.
177177
@classmethod
178178
@override
179-
def construct(
179+
def construct( # pyright: ignore[reportIncompatibleMethodOverride]
180180
cls: Type[ModelT],
181181
_fields_set: set[str] | None = None,
182182
**values: object,
@@ -248,8 +248,8 @@ def model_dump(
248248
self,
249249
*,
250250
mode: Literal["json", "python"] | str = "python",
251-
include: IncEx = None,
252-
exclude: IncEx = None,
251+
include: IncEx | None = None,
252+
exclude: IncEx | None = None,
253253
by_alias: bool = False,
254254
exclude_unset: bool = False,
255255
exclude_defaults: bool = False,
@@ -303,8 +303,8 @@ def model_dump_json(
303303
self,
304304
*,
305305
indent: int | None = None,
306-
include: IncEx = None,
307-
exclude: IncEx = None,
306+
include: IncEx | None = None,
307+
exclude: IncEx | None = None,
308308
by_alias: bool = False,
309309
exclude_unset: bool = False,
310310
exclude_defaults: bool = False,

‎src/browserbase/_types.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
Optional,
1717
Sequence,
1818
)
19-
from typing_extensions import Literal, Protocol, TypeAlias, TypedDict, override, runtime_checkable
19+
from typing_extensions import Set, Literal, Protocol, TypeAlias, TypedDict, override, runtime_checkable
2020

2121
import httpx
2222
import pydantic
@@ -193,7 +193,9 @@ def get(self, __key: str) -> str | None: ...
193193

194194
# Note: copied from Pydantic
195195
# https://github.com/pydantic/pydantic/blob/32ea570bf96e84234d2992e1ddf40ab8a565925a/pydantic/main.py#L49
196-
IncEx: TypeAlias = "set[int] | set[str] | dict[int, Any] | dict[str, Any] | None"
196+
IncEx: TypeAlias = Union[
197+
Set[int], Set[str], Mapping[int, Union["IncEx", Literal[True]]], Mapping[str, Union["IncEx", Literal[True]]]
198+
]
197199

198200
PostParser = Callable[[Any], Any]
199201

‎tests/conftest.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from __future__ import annotations
22

33
import os
4-
import asyncio
54
import logging
65
from typing import TYPE_CHECKING, Iterator, AsyncIterator
76

87
import pytest
8+
from pytest_asyncio import is_async_test
99

1010
from browserbase import Browserbase, AsyncBrowserbase
1111

@@ -17,11 +17,13 @@
1717
logging.getLogger("browserbase").setLevel(logging.DEBUG)
1818

1919

20-
@pytest.fixture(scope="session")
21-
def event_loop() -> Iterator[asyncio.AbstractEventLoop]:
22-
loop = asyncio.new_event_loop()
23-
yield loop
24-
loop.close()
20+
# automatically add `pytest.mark.asyncio()` to all of our async tests
21+
# so we don't have to add that boilerplate everywhere
22+
def pytest_collection_modifyitems(items: list[pytest.Function]) -> None:
23+
pytest_asyncio_tests = (item for item in items if is_async_test(item))
24+
session_scope_marker = pytest.mark.asyncio(loop_scope="session")
25+
for async_test in pytest_asyncio_tests:
26+
async_test.add_marker(session_scope_marker, append=False)
2527

2628

2729
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")

‎tests/test_client.py

+20
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,16 @@ def test_base_url_env(self) -> None:
556556
client = Browserbase(api_key=api_key, _strict_response_validation=True)
557557
assert client.base_url == "http://localhost:5000/from/env/"
558558

559+
# explicit environment arg requires explicitness
560+
with update_env(BROWSERBASE_BASE_URL="http://localhost:5000/from/env"):
561+
with pytest.raises(ValueError, match=r"you must pass base_url=None"):
562+
Browserbase(api_key=api_key, _strict_response_validation=True, environment="production")
563+
564+
client = Browserbase(
565+
base_url=None, api_key=api_key, _strict_response_validation=True, environment="production"
566+
)
567+
assert str(client.base_url).startswith("https://api.browserbase.com")
568+
559569
@pytest.mark.parametrize(
560570
"client",
561571
[
@@ -1332,6 +1342,16 @@ def test_base_url_env(self) -> None:
13321342
client = AsyncBrowserbase(api_key=api_key, _strict_response_validation=True)
13331343
assert client.base_url == "http://localhost:5000/from/env/"
13341344

1345+
# explicit environment arg requires explicitness
1346+
with update_env(BROWSERBASE_BASE_URL="http://localhost:5000/from/env"):
1347+
with pytest.raises(ValueError, match=r"you must pass base_url=None"):
1348+
AsyncBrowserbase(api_key=api_key, _strict_response_validation=True, environment="production")
1349+
1350+
client = AsyncBrowserbase(
1351+
base_url=None, api_key=api_key, _strict_response_validation=True, environment="production"
1352+
)
1353+
assert str(client.base_url).startswith("https://api.browserbase.com")
1354+
13351355
@pytest.mark.parametrize(
13361356
"client",
13371357
[

0 commit comments

Comments
 (0)
Please sign in to comment.