From b17a3b8e6984447421a7581ca56c0521cb3b55dd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 31 Oct 2024 22:18:20 +0000 Subject: [PATCH 1/8] feat(api): api update (#48) --- .stats.yml | 2 +- src/browserbase/resources/projects.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 1a6b2b5..b04bed2 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-b341dd9d5bb77c4f217b94b186763e730fd798fbb773a5e90bb4e2a8d4a2c822.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-d8e42f141c0955e8100ca3ce041ce8dedf5dcf68b04e554a5704e4c2003c2fd4.yml diff --git a/src/browserbase/resources/projects.py b/src/browserbase/resources/projects.py index f8b1936..bf4a5df 100644 --- a/src/browserbase/resources/projects.py +++ b/src/browserbase/resources/projects.py @@ -84,7 +84,7 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ProjectListResponse: - """List all projects""" + """List projects""" return self._get( "/v1/projects", options=make_request_options( @@ -190,7 +190,7 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> ProjectListResponse: - """List all projects""" + """List projects""" return await self._get( "/v1/projects", options=make_request_options( From dc2da25d2e33d55e5655cbb8000fd4afdd6bbf62 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 5 Nov 2024 23:36:53 +0000 Subject: [PATCH 2/8] feat(api): api update (#51) --- .stats.yml | 2 +- README.md | 4 ++-- pyproject.toml | 5 ++--- src/browserbase/_compat.py | 6 ++++-- src/browserbase/_models.py | 9 +++++--- src/browserbase/_utils/__init__.py | 1 + src/browserbase/_utils/_transform.py | 9 ++++++-- src/browserbase/_utils/_utils.py | 17 +++++++++++++++ .../types/sessions/session_recording.py | 2 -- tests/test_models.py | 21 +++++++------------ tests/test_transform.py | 15 +++++++++++++ 11 files changed, 62 insertions(+), 29 deletions(-) diff --git a/.stats.yml b/.stats.yml index b04bed2..d42b050 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-d8e42f141c0955e8100ca3ce041ce8dedf5dcf68b04e554a5704e4c2003c2fd4.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fbrowserbase-7f88912695bab2b98cb73137e6f36125d02fdfaf8eed4532ee1c82385609a259.yml diff --git a/README.md b/README.md index c790a0f..2ddc05b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![PyPI version](https://img.shields.io/pypi/v/browserbase.svg)](https://pypi.org/project/browserbase/) -The Browserbase Python library provides convenient access to the Browserbase REST API from any Python 3.7+ +The Browserbase Python library provides convenient access to the Browserbase REST API from any Python 3.8+ application. The library includes type definitions for all request params and response fields, and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). @@ -340,7 +340,7 @@ print(browserbase.__version__) ## Requirements -Python 3.7 or higher. +Python 3.8 or higher. ## Contributing diff --git a/pyproject.toml b/pyproject.toml index 9cdf351..03560f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,11 +16,10 @@ dependencies = [ "sniffio", "cached-property; python_version < '3.8'", ] -requires-python = ">= 3.7" +requires-python = ">= 3.8" classifiers = [ "Typing :: Typed", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -148,7 +147,7 @@ filterwarnings = [ # there are a couple of flags that are still disabled by # default in strict mode as they are experimental and niche. typeCheckingMode = "strict" -pythonVersion = "3.7" +pythonVersion = "3.8" exclude = [ "_dev", diff --git a/src/browserbase/_compat.py b/src/browserbase/_compat.py index d89920d..4794129 100644 --- a/src/browserbase/_compat.py +++ b/src/browserbase/_compat.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload from datetime import date, datetime -from typing_extensions import Self +from typing_extensions import Self, Literal import pydantic from pydantic.fields import FieldInfo @@ -137,9 +137,11 @@ def model_dump( exclude_unset: bool = False, exclude_defaults: bool = False, warnings: bool = True, + mode: Literal["json", "python"] = "python", ) -> dict[str, Any]: - if PYDANTIC_V2: + if PYDANTIC_V2 or hasattr(model, "model_dump"): return model.model_dump( + mode=mode, exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, diff --git a/src/browserbase/_models.py b/src/browserbase/_models.py index 42551b7..6cb469e 100644 --- a/src/browserbase/_models.py +++ b/src/browserbase/_models.py @@ -37,6 +37,7 @@ PropertyInfo, is_list, is_given, + json_safe, lru_cache, is_mapping, parse_date, @@ -279,8 +280,8 @@ def model_dump( Returns: A dictionary representation of the model. """ - if mode != "python": - raise ValueError("mode is only supported in Pydantic v2") + if mode not in {"json", "python"}: + raise ValueError("mode must be either 'json' or 'python'") if round_trip != False: raise ValueError("round_trip is only supported in Pydantic v2") if warnings != True: @@ -289,7 +290,7 @@ def model_dump( raise ValueError("context is only supported in Pydantic v2") if serialize_as_any != False: raise ValueError("serialize_as_any is only supported in Pydantic v2") - return super().dict( # pyright: ignore[reportDeprecated] + dumped = super().dict( # pyright: ignore[reportDeprecated] include=include, exclude=exclude, by_alias=by_alias, @@ -298,6 +299,8 @@ def model_dump( exclude_none=exclude_none, ) + return cast(dict[str, Any], json_safe(dumped)) if mode == "json" else dumped + @override def model_dump_json( self, diff --git a/src/browserbase/_utils/__init__.py b/src/browserbase/_utils/__init__.py index 3efe66c..a7cff3c 100644 --- a/src/browserbase/_utils/__init__.py +++ b/src/browserbase/_utils/__init__.py @@ -6,6 +6,7 @@ is_list as is_list, is_given as is_given, is_tuple as is_tuple, + json_safe as json_safe, lru_cache as lru_cache, is_mapping as is_mapping, is_tuple_t as is_tuple_t, diff --git a/src/browserbase/_utils/_transform.py b/src/browserbase/_utils/_transform.py index 47e262a..d7c0534 100644 --- a/src/browserbase/_utils/_transform.py +++ b/src/browserbase/_utils/_transform.py @@ -173,6 +173,11 @@ def _transform_recursive( # Iterable[T] or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) ): + # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually + # intended as an iterable, so we don't transform it. + if isinstance(data, dict): + return cast(object, data) + inner_type = extract_type_arg(stripped_type, 0) return [_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] @@ -186,7 +191,7 @@ def _transform_recursive( return data if isinstance(data, pydantic.BaseModel): - return model_dump(data, exclude_unset=True) + return model_dump(data, exclude_unset=True, mode="json") annotated_type = _get_annotated_type(annotation) if annotated_type is None: @@ -324,7 +329,7 @@ async def _async_transform_recursive( return data if isinstance(data, pydantic.BaseModel): - return model_dump(data, exclude_unset=True) + return model_dump(data, exclude_unset=True, mode="json") annotated_type = _get_annotated_type(annotation) if annotated_type is None: diff --git a/src/browserbase/_utils/_utils.py b/src/browserbase/_utils/_utils.py index 0bba17c..e5811bb 100644 --- a/src/browserbase/_utils/_utils.py +++ b/src/browserbase/_utils/_utils.py @@ -16,6 +16,7 @@ overload, ) from pathlib import Path +from datetime import date, datetime from typing_extensions import TypeGuard import sniffio @@ -395,3 +396,19 @@ def lru_cache(*, maxsize: int | None = 128) -> Callable[[CallableT], CallableT]: maxsize=maxsize, ) return cast(Any, wrapper) # type: ignore[no-any-return] + + +def json_safe(data: object) -> object: + """Translates a mapping / sequence recursively in the same fashion + as `pydantic` v2's `model_dump(mode="json")`. + """ + if is_mapping(data): + return {json_safe(key): json_safe(value) for key, value in data.items()} + + if is_iterable(data) and not isinstance(data, (str, bytes, bytearray)): + return [json_safe(item) for item in data] + + if isinstance(data, (datetime, date)): + return data.isoformat() + + return data diff --git a/src/browserbase/types/sessions/session_recording.py b/src/browserbase/types/sessions/session_recording.py index d3e0325..c847137 100644 --- a/src/browserbase/types/sessions/session_recording.py +++ b/src/browserbase/types/sessions/session_recording.py @@ -10,8 +10,6 @@ class SessionRecording(BaseModel): - id: str - data: Dict[str, object] """ See diff --git a/tests/test_models.py b/tests/test_models.py index 5b8044f..c199e94 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -520,19 +520,15 @@ class Model(BaseModel): assert m3.to_dict(exclude_none=True) == {} assert m3.to_dict(exclude_defaults=True) == {} - if PYDANTIC_V2: - - class Model2(BaseModel): - created_at: datetime + class Model2(BaseModel): + created_at: datetime - time_str = "2024-03-21T11:39:01.275859" - m4 = Model2.construct(created_at=time_str) - assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)} - assert m4.to_dict(mode="json") == {"created_at": time_str} - else: - with pytest.raises(ValueError, match="mode is only supported in Pydantic v2"): - m.to_dict(mode="json") + time_str = "2024-03-21T11:39:01.275859" + m4 = Model2.construct(created_at=time_str) + assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)} + assert m4.to_dict(mode="json") == {"created_at": time_str} + if not PYDANTIC_V2: with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): m.to_dict(warnings=False) @@ -558,9 +554,6 @@ class Model(BaseModel): assert m3.model_dump(exclude_none=True) == {} if not PYDANTIC_V2: - with pytest.raises(ValueError, match="mode is only supported in Pydantic v2"): - m.model_dump(mode="json") - with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): m.model_dump(round_trip=True) diff --git a/tests/test_transform.py b/tests/test_transform.py index 436b818..03c2ecd 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -177,17 +177,32 @@ class DateDict(TypedDict, total=False): foo: Annotated[date, PropertyInfo(format="iso8601")] +class DatetimeModel(BaseModel): + foo: datetime + + +class DateModel(BaseModel): + foo: Optional[date] + + @parametrize @pytest.mark.asyncio async def test_iso8601_format(use_async: bool) -> None: dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + tz = "Z" if PYDANTIC_V2 else "+00:00" assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap] + assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692" + tz} # type: ignore[comparison-overlap] dt = dt.replace(tzinfo=None) assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692"} # type: ignore[comparison-overlap] + assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692"} # type: ignore[comparison-overlap] assert await transform({"foo": None}, DateDict, use_async) == {"foo": None} # type: ignore[comparison-overlap] + assert await transform(DateModel(foo=None), Any, use_async) == {"foo": None} # type: ignore assert await transform({"foo": date.fromisoformat("2023-02-23")}, DateDict, use_async) == {"foo": "2023-02-23"} # type: ignore[comparison-overlap] + assert await transform(DateModel(foo=date.fromisoformat("2023-02-23")), DateDict, use_async) == { + "foo": "2023-02-23" + } # type: ignore[comparison-overlap] @parametrize From b1684fa889aecf2fe7965a37ebd9c73977136ef6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:48:16 +0000 Subject: [PATCH 3/8] chore: rebuild project due to codegen change (#53) --- tests/test_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index c70ef50..9cf62b3 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -703,7 +703,7 @@ class Model(BaseModel): [3, "", 0.5], [2, "", 0.5 * 2.0], [1, "", 0.5 * 4.0], - [-1100, "", 7.8], # test large number potentially overflowing + [-1100, "", 8], # test large number potentially overflowing ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) @@ -1482,7 +1482,7 @@ class Model(BaseModel): [3, "", 0.5], [2, "", 0.5 * 2.0], [1, "", 0.5 * 4.0], - [-1100, "", 7.8], # test large number potentially overflowing + [-1100, "", 8], # test large number potentially overflowing ], ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) From e6a41dab6f0de6894a97067611166b1bc61893a2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 11:10:26 +0000 Subject: [PATCH 4/8] chore: rebuild project due to codegen change (#54) --- src/browserbase/resources/sessions/sessions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index fc4cac3..45ef082 100644 --- a/src/browserbase/resources/sessions/sessions.py +++ b/src/browserbase/resources/sessions/sessions.py @@ -155,7 +155,7 @@ def create( "keep_alive": keep_alive, "proxies": proxies, "region": region, - "timeout": api_timeout, + "api_timeout": api_timeout, }, session_create_params.SessionCreateParams, ), @@ -409,7 +409,7 @@ async def create( "keep_alive": keep_alive, "proxies": proxies, "region": region, - "timeout": api_timeout, + "api_timeout": api_timeout, }, session_create_params.SessionCreateParams, ), From ff1708757bdeaa4e6b8d1959d1830105bd7f4b92 Mon Sep 17 00:00:00 2001 From: Stainless Bot Date: Fri, 15 Nov 2024 17:28:09 +0000 Subject: [PATCH 5/8] chore: rebuild project due to codegen change (#55) From dfd0e199c2447d4bd1b6704745d22f959a6b6bb1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 Nov 2024 12:09:01 +0000 Subject: [PATCH 6/8] chore: rebuild project due to codegen change (#57) --- src/browserbase/_utils/_transform.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/browserbase/_utils/_transform.py b/src/browserbase/_utils/_transform.py index d7c0534..a6b62ca 100644 --- a/src/browserbase/_utils/_transform.py +++ b/src/browserbase/_utils/_transform.py @@ -316,6 +316,11 @@ async def _async_transform_recursive( # Iterable[T] or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) ): + # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually + # intended as an iterable, so we don't transform it. + if isinstance(data, dict): + return cast(object, data) + inner_type = extract_type_arg(stripped_type, 0) return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] From f3be0bec13d95c65ab4cc81565b456cb566a62e2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:26:51 +0000 Subject: [PATCH 7/8] chore: rebuild project due to codegen change (#58) --- tests/api_resources/test_sessions.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 8ebd5da..7b9fbce 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -41,11 +41,11 @@ def test_method_create_with_all_params(self, client: Browserbase) -> None: }, "extension_id": "extensionId", "fingerprint": { - "browsers": ["chrome", "edge", "firefox"], - "devices": ["desktop", "mobile"], + "browsers": ["chrome"], + "devices": ["desktop"], "http_version": 1, - "locales": ["string", "string", "string"], - "operating_systems": ["android", "ios", "linux"], + "locales": ["string"], + "operating_systems": ["android"], "screen": { "max_height": 0, "max_width": 0, @@ -270,11 +270,11 @@ async def test_method_create_with_all_params(self, async_client: AsyncBrowserbas }, "extension_id": "extensionId", "fingerprint": { - "browsers": ["chrome", "edge", "firefox"], - "devices": ["desktop", "mobile"], + "browsers": ["chrome"], + "devices": ["desktop"], "http_version": 1, - "locales": ["string", "string", "string"], - "operating_systems": ["android", "ios", "linux"], + "locales": ["string"], + "operating_systems": ["android"], "screen": { "max_height": 0, "max_width": 0, From 7001a209fdfe7c610dd4c3a436afa3fb59b2968d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:27:07 +0000 Subject: [PATCH 8/8] release: 1.0.1 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 18 ++++++++++++++++++ pyproject.toml | 2 +- src/browserbase/_version.py | 2 +- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fea3454..a8f7122 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.0.0" + ".": "1.0.1" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a17bbc5..0df64a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 1.0.1 (2024-11-18) + +Full Changelog: [v1.0.0...v1.0.1](https://github.com/browserbase/sdk-python/compare/v1.0.0...v1.0.1) + +### Features + +* **api:** api update ([#48](https://github.com/browserbase/sdk-python/issues/48)) ([b17a3b8](https://github.com/browserbase/sdk-python/commit/b17a3b8e6984447421a7581ca56c0521cb3b55dd)) +* **api:** api update ([#51](https://github.com/browserbase/sdk-python/issues/51)) ([dc2da25](https://github.com/browserbase/sdk-python/commit/dc2da25d2e33d55e5655cbb8000fd4afdd6bbf62)) + + +### Chores + +* rebuild project due to codegen change ([#53](https://github.com/browserbase/sdk-python/issues/53)) ([b1684fa](https://github.com/browserbase/sdk-python/commit/b1684fa889aecf2fe7965a37ebd9c73977136ef6)) +* rebuild project due to codegen change ([#54](https://github.com/browserbase/sdk-python/issues/54)) ([e6a41da](https://github.com/browserbase/sdk-python/commit/e6a41dab6f0de6894a97067611166b1bc61893a2)) +* rebuild project due to codegen change ([#55](https://github.com/browserbase/sdk-python/issues/55)) ([ff17087](https://github.com/browserbase/sdk-python/commit/ff1708757bdeaa4e6b8d1959d1830105bd7f4b92)) +* rebuild project due to codegen change ([#57](https://github.com/browserbase/sdk-python/issues/57)) ([dfd0e19](https://github.com/browserbase/sdk-python/commit/dfd0e199c2447d4bd1b6704745d22f959a6b6bb1)) +* rebuild project due to codegen change ([#58](https://github.com/browserbase/sdk-python/issues/58)) ([f3be0be](https://github.com/browserbase/sdk-python/commit/f3be0bec13d95c65ab4cc81565b456cb566a62e2)) + ## 1.0.0 (2024-10-29) Full Changelog: [v1.0.0-alpha.0...v1.0.0](https://github.com/browserbase/sdk-python/compare/v1.0.0-alpha.0...v1.0.0) diff --git a/pyproject.toml b/pyproject.toml index 03560f5..2e4cc0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browserbase" -version = "1.0.0" +version = "1.0.1" 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 1f27c64..0942842 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__ = "1.0.0" # x-release-please-version +__version__ = "1.0.1" # x-release-please-version