From 2e886523c5a1d8ce60a46b82ca66881c39f95321 Mon Sep 17 00:00:00 2001 From: Leo Date: Mon, 27 Jul 2026 16:52:16 +0200 Subject: [PATCH] feat(sdk): add profile duplication --- .../src/notte_sdk/endpoints/profiles.py | 27 ++++++++++++++++ packages/notte-sdk/src/notte_sdk/types.py | 13 ++++++++ tests/sdk/test_profile_endpoint_paths.py | 32 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 tests/sdk/test_profile_endpoint_paths.py diff --git a/packages/notte-sdk/src/notte_sdk/endpoints/profiles.py b/packages/notte-sdk/src/notte_sdk/endpoints/profiles.py index 63f060a7d..2a7fd2df9 100644 --- a/packages/notte-sdk/src/notte_sdk/endpoints/profiles.py +++ b/packages/notte-sdk/src/notte_sdk/endpoints/profiles.py @@ -11,6 +11,8 @@ ExecutionResponse, ProfileCreateRequest, ProfileCreateRequestDict, + ProfileDuplicateRequest, + ProfileDuplicateRequestDict, ProfileListRequest, ProfileListRequestDict, ProfileResponse, @@ -28,6 +30,7 @@ class ProfilesClient(BaseClient): CREATE_PROFILE = "create" GET_PROFILE = "{profile_id}" DELETE_PROFILE = "{profile_id}" + DUPLICATE_PROFILE = "{profile_id}/duplicate" LIST_PROFILES = "" def __init__( @@ -80,6 +83,15 @@ def _delete_profile_endpoint(profile_id: str) -> NotteEndpoint[ExecutionResponse method="DELETE", ) + @staticmethod + def _duplicate_profile_endpoint(profile_id: str) -> NotteEndpoint[ProfileResponse]: + """Returns a NotteEndpoint configured for duplicating a profile.""" + return NotteEndpoint( + path=ProfilesClient.DUPLICATE_PROFILE.format(profile_id=profile_id), + response=ProfileResponse, + method="POST", + ) + @staticmethod def _list_profiles_endpoint() -> NotteEndpoint[ProfileResponse]: """Returns a NotteEndpoint configured for listing profiles.""" @@ -139,6 +151,21 @@ def delete(self, profile_id: str) -> bool: result = self.request(ProfilesClient._delete_profile_endpoint(profile_id)) return result.success + @track_usage("cloud.profile.duplicate") + def duplicate(self, profile_id: str, **data: Unpack[ProfileDuplicateRequestDict]) -> ProfileResponse: + """Duplicate a profile and its last persisted browser state. + + Args: + profile_id: Source profile ID + **data: Optional destination name. The source name is reused when omitted. + + Returns: + ProfileResponse: The newly created profile + """ + request = ProfileDuplicateRequest.model_validate(data) + endpoint = ProfilesClient._duplicate_profile_endpoint(profile_id).with_request(request) + return self.request(endpoint) + @track_usage("cloud.profile.list") def list(self, **data: Unpack[ProfileListRequestDict]) -> Sequence[ProfileResponse]: """List all profiles for the authenticated user. diff --git a/packages/notte-sdk/src/notte_sdk/types.py b/packages/notte-sdk/src/notte_sdk/types.py index 0f1f17a93..0f6c29191 100644 --- a/packages/notte-sdk/src/notte_sdk/types.py +++ b/packages/notte-sdk/src/notte_sdk/types.py @@ -1380,6 +1380,19 @@ class ProfileCreateRequest(SdkRequest): name: Annotated[str | None, Field(description="Name of the profile")] = None +class ProfileDuplicateRequestDict(TypedDict, total=False): + """Request dictionary for duplicating a profile.""" + + name: str + + +class ProfileDuplicateRequest(SdkRequest): + name: Annotated[ + str | None, + Field(description="Optional name for the duplicate; defaults to the source profile name"), + ] = None + + class ProfileResponse(SdkResponse): profile_id: Annotated[str, Field(description="Profile ID (format: notte-profile-{16 hex chars})")] name: Annotated[str | None, Field(description="Profile name")] diff --git a/tests/sdk/test_profile_endpoint_paths.py b/tests/sdk/test_profile_endpoint_paths.py new file mode 100644 index 000000000..3c5408854 --- /dev/null +++ b/tests/sdk/test_profile_endpoint_paths.py @@ -0,0 +1,32 @@ +import datetime as dt +from unittest.mock import patch + +from notte_sdk import NotteClient +from notte_sdk.endpoints.profiles import ProfilesClient +from notte_sdk.types import ProfileDuplicateRequest, ProfileResponse + + +def test_profile_duplicate_endpoint_uses_source_id() -> None: + endpoint = ProfilesClient._duplicate_profile_endpoint("notte-profile-source") + + assert endpoint.method == "POST" + assert endpoint.path == "notte-profile-source/duplicate" + assert endpoint.response is ProfileResponse + + +def test_profile_duplicate_sends_optional_destination_name() -> None: + client = NotteClient(api_key="test-api-key", server_url="https://api.notte.cc") + expected = ProfileResponse( + profile_id="notte-profile-copy", + name="Copied login", + created_at=dt.datetime(2026, 7, 27, tzinfo=dt.UTC), + updated_at=dt.datetime(2026, 7, 27, tzinfo=dt.UTC), + ) + + with patch.object(client.profiles, "request", return_value=expected) as request: + result = client.profiles.duplicate("notte-profile-source", name="Copied login") + + assert result is expected + endpoint = request.call_args.args[0] + assert endpoint.path == "notte-profile-source/duplicate" + assert endpoint.request == ProfileDuplicateRequest(name="Copied login")