From 78b60a3803fb0629dc66d76b89f1be1765a60a52 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 15 Aug 2026 23:22:54 +0000 Subject: [PATCH 1/2] Add volumes.delete for DELETE /volume/:key Co-authored-by: shri --- README.md | 1 + .../client/managers/async_manager/volume.py | 7 ++++ .../client/managers/sync_manager/volume.py | 7 ++++ hyperbrowser/models/__init__.py | 9 ++++- hyperbrowser/models/volume.py | 6 +++ tests/test_volume_wire_contract.py | 37 ++++++++++++++++++- tests/typecheck/valid_requests.py | 2 + 7 files changed, 67 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 667155e6..8f043836 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,7 @@ sandbox = client.sandboxes.create( ) sandbox.stop() +client.volumes.delete(same_volume.id) client.close() ``` diff --git a/hyperbrowser/client/managers/async_manager/volume.py b/hyperbrowser/client/managers/async_manager/volume.py index acec97ac..52e8649e 100644 --- a/hyperbrowser/client/managers/async_manager/volume.py +++ b/hyperbrowser/client/managers/async_manager/volume.py @@ -4,6 +4,7 @@ from hyperbrowser.models.volume import ( CreateVolumeParams, Volume, + VolumeDeleteResult, VolumeListParams, VolumeListResponse, ) @@ -43,3 +44,9 @@ async def get(self, volume_id: str) -> Volume: self._client._build_url(f"/volume/{volume_id}") ) return Volume(**response.data) + + async def delete(self, volume_id: str) -> VolumeDeleteResult: + response = await self._client.transport.delete( + self._client._build_url(f"/volume/{volume_id}") + ) + return VolumeDeleteResult(**response.data) diff --git a/hyperbrowser/client/managers/sync_manager/volume.py b/hyperbrowser/client/managers/sync_manager/volume.py index 59f7e205..85f7bfa2 100644 --- a/hyperbrowser/client/managers/sync_manager/volume.py +++ b/hyperbrowser/client/managers/sync_manager/volume.py @@ -4,6 +4,7 @@ from hyperbrowser.models.volume import ( CreateVolumeParams, Volume, + VolumeDeleteResult, VolumeListParams, VolumeListResponse, ) @@ -43,3 +44,9 @@ def get(self, volume_id: str) -> Volume: self._client._build_url(f"/volume/{volume_id}") ) return Volume(**response.data) + + def delete(self, volume_id: str) -> VolumeDeleteResult: + response = self._client.transport.delete( + self._client._build_url(f"/volume/{volume_id}") + ) + return VolumeDeleteResult(**response.data) diff --git a/hyperbrowser/models/__init__.py b/hyperbrowser/models/__init__.py index 04678b09..abbaa9f7 100644 --- a/hyperbrowser/models/__init__.py +++ b/hyperbrowser/models/__init__.py @@ -210,7 +210,13 @@ ProfileListResponse, ProfileResponse, ) -from .volume import CreateVolumeParams, Volume, VolumeListParams, VolumeListResponse +from .volume import ( + CreateVolumeParams, + Volume, + VolumeDeleteResult, + VolumeListParams, + VolumeListResponse, +) from .scrape import ( BatchScrapeJobResponse, BatchScrapeJobStatusResponse, @@ -537,6 +543,7 @@ "Volume", "VolumeListParams", "VolumeListResponse", + "VolumeDeleteResult", # scrape "BatchScrapeJobResponse", "BatchScrapeJobStatusResponse", diff --git a/hyperbrowser/models/volume.py b/hyperbrowser/models/volume.py index c857d86f..7289e871 100644 --- a/hyperbrowser/models/volume.py +++ b/hyperbrowser/models/volume.py @@ -36,3 +36,9 @@ class VolumeListResponse(VolumeBaseModel): total_count: Optional[int] = Field(default=None, alias="totalCount") page: Optional[int] = None per_page: Optional[int] = Field(default=None, alias="perPage") + + +class VolumeDeleteResult(VolumeBaseModel): + deleted: bool + id: Optional[str] = None + name: Optional[str] = None diff --git a/tests/test_volume_wire_contract.py b/tests/test_volume_wire_contract.py index de3d283c..5356affd 100644 --- a/tests/test_volume_wire_contract.py +++ b/tests/test_volume_wire_contract.py @@ -4,7 +4,12 @@ VolumeManager as AsyncVolumeManager, ) from hyperbrowser.client.managers.sync_manager.volume import VolumeManager -from hyperbrowser.models import CreateVolumeParams, Volume, VolumeListParams +from hyperbrowser.models import ( + CreateVolumeParams, + Volume, + VolumeDeleteResult, + VolumeListParams, +) VOLUME_PAYLOAD = { @@ -26,6 +31,12 @@ "perPage": 20, } +VOLUME_DELETE_PAYLOAD = { + "deleted": True, + "id": "2d6f01cf-c5d7-4c61-ae9e-0264f1c8063d", + "name": "project-cache", +} + class StubResponse: def __init__(self, data): @@ -55,6 +66,10 @@ def get(self, url, params=None, follow_redirects=False): return StubResponse(VOLUME_DETAIL_PAYLOAD) return StubResponse({}) + def delete(self, url): + self.calls.append({"method": "DELETE", "url": url}) + return StubResponse(VOLUME_DELETE_PAYLOAD) + class RecordingAsyncTransport: def __init__(self): @@ -79,6 +94,10 @@ async def get(self, url, params=None, follow_redirects=False): return StubResponse(VOLUME_DETAIL_PAYLOAD) return StubResponse({}) + async def delete(self, url): + self.calls.append({"method": "DELETE", "url": url}) + return StubResponse(VOLUME_DELETE_PAYLOAD) + class FakeSyncClient: def __init__(self): @@ -131,10 +150,12 @@ def test_sync_volume_manager_uses_expected_wire_keys(use_legacy_model): created = manager.create(CreateVolumeParams(name="project-cache")) listed = manager.list(list_params) fetched = manager.get("2d6f01cf-c5d7-4c61-ae9e-0264f1c8063d") + deleted = manager.delete("2d6f01cf-c5d7-4c61-ae9e-0264f1c8063d") create_call = client.transport.calls[0] list_call = client.transport.calls[1] get_call = client.transport.calls[2] + delete_call = client.transport.calls[3] assert create_call["method"] == "POST" assert create_call["url"].endswith("/volume") @@ -153,6 +174,13 @@ def test_sync_volume_manager_uses_expected_wire_keys(use_legacy_model): assert fetched.name == "project-cache" assert fetched.transfer_amount is None + assert delete_call["method"] == "DELETE" + assert delete_call["url"].endswith("/volume/2d6f01cf-c5d7-4c61-ae9e-0264f1c8063d") + assert deleted.deleted is True + assert deleted.id == "2d6f01cf-c5d7-4c61-ae9e-0264f1c8063d" + assert deleted.name == "project-cache" + assert isinstance(deleted, VolumeDeleteResult) + @pytest.mark.anyio @pytest.mark.parametrize("use_legacy_model", [False, True]) @@ -166,10 +194,12 @@ async def test_async_volume_manager_uses_expected_wire_keys(use_legacy_model): created = await manager.create({"name": "project-cache"}) listed = await manager.list(list_params) fetched = await manager.get("2d6f01cf-c5d7-4c61-ae9e-0264f1c8063d") + deleted = await manager.delete("2d6f01cf-c5d7-4c61-ae9e-0264f1c8063d") create_call = client.transport.calls[0] list_call = client.transport.calls[1] get_call = client.transport.calls[2] + delete_call = client.transport.calls[3] assert create_call["method"] == "POST" assert create_call["url"].endswith("/volume") @@ -186,6 +216,11 @@ async def test_async_volume_manager_uses_expected_wire_keys(use_legacy_model): assert created.transfer_amount == 0 assert fetched.name == "project-cache" + assert delete_call["method"] == "DELETE" + assert delete_call["url"].endswith("/volume/2d6f01cf-c5d7-4c61-ae9e-0264f1c8063d") + assert deleted.deleted is True + assert deleted.name == "project-cache" + def test_sync_volume_list_without_params_remains_supported(): client = FakeSyncClient() diff --git a/tests/typecheck/valid_requests.py b/tests/typecheck/valid_requests.py index 34e9fdcd..3512237e 100644 --- a/tests/typecheck/valid_requests.py +++ b/tests/typecheck/valid_requests.py @@ -133,6 +133,7 @@ def valid_sync_requests(client: Hyperbrowser) -> None: ) client.volumes.list({"page": 0, "limit": -1}) client.volumes.list(LegacyVolumeListParams(page=0, limit=-1)) + client.volumes.delete("2d6f01cf-c5d7-4c61-ae9e-0264f1c8063d") client.sessions.create(LegacyCreateSessionParams(use_stealth=True, region="us")) client.web.fetch(LegacyFetchParams(url="https://example.com")) @@ -198,6 +199,7 @@ async def valid_async_requests(client: AsyncHyperbrowser) -> None: ) await client.sandboxes.list_image_builds({"status": "verifying", "limit": -1}) await client.volumes.list({"page": 0, "limit": -1}) + await client.volumes.delete("2d6f01cf-c5d7-4c61-ae9e-0264f1c8063d") await client.sessions.create(LegacyCreateSessionParams(use_proxy=True, region="us")) await client.web.fetch(LegacyFetchParams(url="https://example.com")) From 4fbe69c5998bb6018452e2fd45918e9798a7abe9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 00:45:54 +0000 Subject: [PATCH 2/2] Document volume delete 409s for ambiguous names Co-authored-by: shri --- hyperbrowser/client/managers/async_manager/volume.py | 1 + hyperbrowser/client/managers/sync_manager/volume.py | 1 + 2 files changed, 2 insertions(+) diff --git a/hyperbrowser/client/managers/async_manager/volume.py b/hyperbrowser/client/managers/async_manager/volume.py index 52e8649e..f87a2086 100644 --- a/hyperbrowser/client/managers/async_manager/volume.py +++ b/hyperbrowser/client/managers/async_manager/volume.py @@ -46,6 +46,7 @@ async def get(self, volume_id: str) -> Volume: return Volume(**response.data) async def delete(self, volume_id: str) -> VolumeDeleteResult: + """Delete a volume by id or name. Ambiguous names and active mounts return 409.""" response = await self._client.transport.delete( self._client._build_url(f"/volume/{volume_id}") ) diff --git a/hyperbrowser/client/managers/sync_manager/volume.py b/hyperbrowser/client/managers/sync_manager/volume.py index 85f7bfa2..f15a9fb7 100644 --- a/hyperbrowser/client/managers/sync_manager/volume.py +++ b/hyperbrowser/client/managers/sync_manager/volume.py @@ -46,6 +46,7 @@ def get(self, volume_id: str) -> Volume: return Volume(**response.data) def delete(self, volume_id: str) -> VolumeDeleteResult: + """Delete a volume by id or name. Ambiguous names and active mounts return 409.""" response = self._client.transport.delete( self._client._build_url(f"/volume/{volume_id}") )