diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fea34540..a8f71224 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/.stats.yml b/.stats.yml index 1a6b2b54..d42b050b 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-7f88912695bab2b98cb73137e6f36125d02fdfaf8eed4532ee1c82385609a259.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index a17bbc56..0df64a39 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/MIGRATION.md b/MIGRATION.md new file mode 100644 index 00000000..4b492059 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,241 @@ +# Migration Guide + +The Browserbase v1 Python SDK has been rewritten from the ground up and ships with a ton of new features and better support that we can't wait for you to try. This guide is designed to help you maximize your experience with v1. + +We hope this guide is useful to you; if you have any questions don't hesitate to reach out to support@browserbase.com or [create a new issue](https://github.com/browserbase/sdk-python/issues/new). + +We've written out specific guidelines on how to migrate each v0 method to v1 below. v1 also adds one-to-one mappings for every API endpoint, so you can incorporate new Browserbase features in your codebase with much less lift. + +## Breaking Changes + +The v1 SDK is more flexible, easier to use, and has a more consistent API. It is also a lot more modular, meaning the majority of function calls have changed from `browserbase.$thing_$do()` to `browserbase.$thing.$do()`. For example: + +```python +# v0 SDK +browserbase.list_sessions() + +# v1 SDK +bb.sessions.list() +``` + +### Deleted Methods + +`load`, `load_url`, and `screenshot` have been fully removed in the v1 SDK. You can use the following example instead that encapsulates the same functionality using Playwright. + +```python +from playwright.sync_api import Playwright, sync_playwright +from browserbase import Browserbase + +bb = Browserbase(api_key=BROWSERBASE_API_KEY) + +def run(playwright: Playwright) -> None: + # Create a session on Browserbase + session = bb.sessions.create(project_id=BROWSERBASE_PROJECT_ID) + + # Connect to the remote session + chromium = playwright.chromium + browser = chromium.connect_over_cdp(session.connect_url) + context = browser.contexts[0] + page = context.pages[0] + + # Execute Playwright actions on the remote browser tab + page.goto("https://news.ycombinator.com/") + page_title = page.title() + assert ( + page_title == "Hacker News" + ), f"Page title is not 'Hacker News', it is '{page_title}'" + page.screenshot(path="screenshot.png") + + page.close() + browser.close() + print(f"Done! View replay at https://browserbase.com/sessions/{session.id}") + + +if __name__ == "__main__": + with sync_playwright() as playwright: + run(playwright) +``` + +For async Playwright (like in Jupyter notebooks or IPython environments), you can import `async_playwright` instead of `sync_playwright`. + +## Updates to Common Workflows + +### Create Session + +This is how you would create a session with the v0 SDK, where `CreateSessionOptions` is a Pydantic object defined [here](https://github.com/browserbase/python-sdk/blob/0a499ba29853f20bb3055d7c81c5f61c24fcd9ec/browserbase/__init__.py#L52). + +```python +# v0 SDK +from browserbase import Browserbase, CreateSessionOptions + +browserbase = Browserbase(api_key=BROWSERBASE_API_KEY, project_id=BROWSERBASE_PROJECT_ID) +options = CreateSessionOptions(extensionId='123') +browserbase.create_session(options) +``` + +Now, you can create a session with the v1 SDK by calling the `create` method on `sessions`. + +```python +# v1 SDK +from browserbase import Browserbase + +bb = Browserbase(api_key=BROWSERBASE_API_KEY) +session = bb.sessions.create(project_id=BROWSERBASE_PROJECT_ID, extension_id="some_extension_id") +``` + +For more complex session creation, you can import `BrowserSettings` and use Pydantic's `TypeAdapter` to conform JSON spec to the appropriate Pydantic class. You can also import each individual subclass. + +```python +# v1 SDK +from browserbase import Browserbase +from pydantic import TypeAdapter +from browserbase.types.session_create_params import BrowserSettings + +session = bb.sessions.create( + project_id=BROWSERBASE_PROJECT_ID, + extension_id="some_extension_id", + browser_settings=TypeAdapter(BrowserSettings).validate_python( + {"context": {"id": context_id, "persist": True}} + ), + ) +``` + +### Get Connect URL + +In the v0 SDK, you could run `browserbase.get_connect_url()` to create a new session and retrieve its connect url, or `browserbase.get_connect_url(session_id=some_session.id)` to retrieve the connect url for an existing session. + +In the v1 SDK, you can create a session and retrieve its connect url in a single call with `bb.sessions.create()`. + +```python +# v0 SDK +from browserbase import Browserbase + +browserbase = Browserbase(api_key=BROWSERBASE_API_KEY, project_id=BROWSERBASE_PROJECT_ID) + +# To create a new session and connect to it +connect_url = browserbase.get_connect_url() + +# To connect to an existing session +connect_url = browserbase.get_connect_url(session_id=some_session.id) +``` + +```python +# v1 SDK +from browserbase import Browserbase +bb = Browserbase(api_key=BROWSERBASE_API_KEY) + +def get_connect_url(bb: Browserbase, session_id: str = None): + """ + Retrieve a connect url for a given session or create a new one. + + If a session id is provided, retrieve the connect url for the existing session. + Otherwise, create a new session and return the connect url. + """ + if session_id: + session = bb.sessions.retrieve(id=session_id) + else: + session = bb.sessions.create(project_id=BROWSERBASE_PROJECT_ID) + return session.connect_url + +connect_url = get_connect_url(bb, session_id="some_session_id") +``` + +### Complete Session + +v0 allowed you to complete a session by calling `browserbase.complete_session(session_id=some_session.id)`. + +```python +# v0 SDK +browserbase.complete_session(session_id=some_session.id) +``` + +In the v1 SDK, completing a session is done by updating its status to `REQUEST_RELEASE`. + +```python +# v1 SDK +bb.sessions.update(id=session_id, status="REQUEST_RELEASE") +``` + +## Reference for other methods + +These methods have been rewritten for modularity and flexibility. As mentioned above, the pattern here is that the method has been renamed from `browserbase.$thing_$do()` to `bb.$thing.$do()`. + +### List Sessions + +```python +# v0 SDK +sessions = browserbase.list_sessions() +``` + +```python +# v1 SDK +sessions = bb.sessions.list() +``` + +### Get Session + +```python +# v0 SDK +session = browserbase.get_session(session_id="some_session_id") +``` + +```python +# v1 SDK +session = bb.sessions.retrieve(id="some_session_id") +``` + +### Get Session Recording + +```python +# v0 SDK +recording = browserbase.get_session_recording(session_id=some_session.id) +``` + +```python +# v1 SDK +recording = bb.sessions.recording.retrieve(id="some_session_id") +``` + +### Get Session Downloads + +**Note:** The parameter `retry_interval` is no longer supported. You can configure retries with the following syntax on bb init: + +```python +bb = Browserbase(api_key=BROWSERBASE_API_KEY, max_retries=5) +``` + +Keep in mind, however, that this only affects the default retry behavior, which will only retry on 4xx/5xx errors. The remaining pattern still applies: + +```python +# v0 SDK +downloads = browserbase.get_session_downloads(session_id=some_session.id) +``` + +```python +# v1 SDK +downloads = bb.sessions.downloads.retrieve(id="some_session_id") +``` + +### Get Debug Connection URLs + +```python +# v0 SDK +debug_urls = browserbase.get_debug_connection_urls(session_id=some_session.id) +``` + +```python +# v1 SDK +debug_urls = bb.sessions.debug.list(id="some_session_id") +``` + +### Get Session Logs + +```python +# v0 SDK +logs = browserbase.get_session_logs(session_id=some_session.id) +``` + +```python +# v1 SDK +logs = bb.sessions.logs.list(id="some_session_id") +``` diff --git a/README.md b/README.md index c790a0f3..2ddc05b6 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 9cdf351a..2e4cc0ec 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" @@ -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 d89920d9..4794129c 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 42551b76..6cb469e2 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 3efe66c8..a7cff3c0 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 47e262a5..a6b62cad 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: @@ -311,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] @@ -324,7 +334,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 0bba17ca..e5811bba 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/_version.py b/src/browserbase/_version.py index 1f27c648..09428425 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 diff --git a/src/browserbase/resources/projects.py b/src/browserbase/resources/projects.py index f8b1936a..bf4a5df9 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( diff --git a/src/browserbase/resources/sessions/sessions.py b/src/browserbase/resources/sessions/sessions.py index fc4cac3c..45ef0820 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, ), diff --git a/src/browserbase/types/sessions/session_recording.py b/src/browserbase/types/sessions/session_recording.py index d3e0325a..c8471371 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/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 8ebd5daf..7b9fbce1 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, diff --git a/tests/test_client.py b/tests/test_client.py index c70ef50e..9cf62b33 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)) diff --git a/tests/test_models.py b/tests/test_models.py index 5b8044f0..c199e942 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 436b8185..03c2ecd4 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