From 3c2b9e2e728b5e245dfcb6eda17453953f9c2bc5 Mon Sep 17 00:00:00 2001 From: jab416171 Date: Fri, 7 Aug 2026 16:25:27 -0600 Subject: [PATCH 1/3] feat: add asset delete --- src/comfy_low/transport.py | 10 ++++++++++ src/comfy_sdk/assets.py | 26 ++++++++++++++++++++++++++ tests/conftest.py | 14 ++++++++++++++ tests/test_assets.py | 29 +++++++++++++++++++++++++++++ tests/test_async.py | 28 ++++++++++++++++++++++++++++ 5 files changed, 107 insertions(+) diff --git a/src/comfy_low/transport.py b/src/comfy_low/transport.py index 207f6bf..a09013a 100644 --- a/src/comfy_low/transport.py +++ b/src/comfy_low/transport.py @@ -341,6 +341,11 @@ def get_asset(self, asset_id: str, *, timeout: Any = _UNSET) -> Asset: resp = self.raw_request("GET", f"/assets/{asset_id}", timeout=timeout) return Asset.model_validate(self._p.parse_or_raise(resp, (200,))) + def delete_asset(self, asset_id: str, *, timeout: Any = _UNSET) -> None: + """DELETE /api/v2/assets/{id} — removes the asset record and its content.""" + resp = self.raw_request("DELETE", f"/assets/{asset_id}", timeout=timeout) + self._p.parse_or_raise(resp, (204,)) + @contextmanager def get_asset_content( self, @@ -617,6 +622,11 @@ async def get_asset(self, asset_id: str, *, timeout: Any = _UNSET) -> Asset: resp = await self.raw_request("GET", f"/assets/{asset_id}", timeout=timeout) return Asset.model_validate(self._p.parse_or_raise(resp, (200,))) + async def delete_asset(self, asset_id: str, *, timeout: Any = _UNSET) -> None: + """DELETE /api/v2/assets/{id} — removes the asset record and its content.""" + resp = await self.raw_request("DELETE", f"/assets/{asset_id}", timeout=timeout) + self._p.parse_or_raise(resp, (204,)) + @asynccontextmanager async def get_asset_content( self, diff --git a/src/comfy_sdk/assets.py b/src/comfy_sdk/assets.py index 9ff9c4b..23d092d 100644 --- a/src/comfy_sdk/assets.py +++ b/src/comfy_sdk/assets.py @@ -128,6 +128,14 @@ def commit(self) -> str: assert self._id is not None return self._id + def delete(self) -> None: + """Delete this asset from storage.""" + if self._id is None: + raise RuntimeError("cannot delete an uncommitted asset") + with translating(): + self._low.delete_asset(self._id) + self._id = None + def as_reference(self) -> dict[str, object]: """The ``core/ASSET`` object (commits first if needed).""" self.commit() @@ -166,6 +174,14 @@ async def commit(self) -> str: assert self._id is not None return self._id + async def delete(self) -> None: + """Delete this asset from storage.""" + if self._id is None: + raise RuntimeError("cannot delete an uncommitted asset") + with translating(): + await self._low.delete_asset(self._id) + self._id = None + async def as_reference(self) -> dict[str, object]: await self.commit() assert self._id is not None @@ -242,6 +258,11 @@ def get(self, asset_id: str) -> Asset: asset._apply(model) return asset + def delete(self, asset_id: str) -> None: + """Delete an asset by UUID.""" + with translating(): + self._low.delete_asset(asset_id) + class AsyncAssetFactory: """``client.assets`` — async alternative constructors for :class:`AsyncAsset`.""" @@ -286,6 +307,11 @@ async def get(self, asset_id: str) -> AsyncAsset: asset._apply(model) return asset + async def delete(self, asset_id: str) -> None: + """Delete an asset by UUID.""" + with translating(): + await self._low.delete_asset(asset_id) + def _no_opener() -> tuple[BinaryIO, int | None]: raise RuntimeError("this asset is already committed; nothing to upload") diff --git a/tests/conftest.py b/tests/conftest.py index b249b9f..2156db0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -58,6 +58,7 @@ class ServerState: upload_count: int = 0 from_hash_count: int = 0 head_count: int = 0 + delete_count: int = 0 job_poll_count: int = 0 events_connect_count: int = 0 submit_count: int = 0 @@ -174,6 +175,19 @@ def do_HEAD(self) -> None: self.send_response(404) self.end_headers() + # -- DELETE -- + def do_DELETE(self) -> None: + if not self._auth_ok(): + self._err(401, "unauthorized", "no key") + return + m = re.match(r"/api/v2/assets/([^/]+)$", self.path) + if m: + state.delete_count += 1 + self.send_response(204) + self.end_headers() + return + self._err(404, "not_found") + # -- GET -- def do_GET(self) -> None: if not self._auth_ok(): diff --git a/tests/test_assets.py b/tests/test_assets.py index 2c51f45..e9f00bb 100644 --- a/tests/test_assets.py +++ b/tests/test_assets.py @@ -102,3 +102,32 @@ def test_hash_mismatch_surfaced_without_blind_retry(server, tmp_path) -> None: # Exactly one upload attempt — a 409 hash_mismatch must not be blindly retried. assert server.state.upload_count == 1 + + +def test_delete_asset_by_id(server) -> None: + with Comfy(server.base_url) as client: + client.assets.delete("asset_uuid_01") + + assert server.state.delete_count == 1 + + +def test_delete_asset_on_asset_instance(server) -> None: + data = b"delete-me-bytes" + with Comfy(server.base_url) as client: + asset = client.assets.from_bytes(data, filename="photo.png") + asset.commit() + asset_id = asset.id + asset.delete() + + assert asset_id == "asset_uploaded_01" + assert server.state.delete_count == 1 + assert asset.id is None + + +def test_delete_uncommitted_asset_raises(server) -> None: + with Comfy(server.base_url) as client: + asset = client.assets.from_bytes(b"not-uploaded", filename="photo.png") + with pytest.raises(RuntimeError, match="uncommitted"): + asset.delete() + + assert server.state.delete_count == 0 diff --git a/tests/test_async.py b/tests/test_async.py index 89ef835..1cbe1d0 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -147,3 +147,31 @@ async def test_async_queue_full_retries_with_retry_after(server) -> None: async with AsyncComfy(server.base_url) as client: await client.submit(_wf(client)) assert server.state.submit_count == 3 + + +async def test_async_delete_asset_by_id(server) -> None: + async with AsyncComfy(server.base_url) as client: + await client.assets.delete("asset_uuid_01") + + assert server.state.delete_count == 1 + + +async def test_async_delete_asset_on_asset_instance(server) -> None: + data = b"async-delete-me-bytes" + async with AsyncComfy(server.base_url) as client: + asset = client.assets.from_bytes(data, filename="photo.png") + asset_id = await asset.commit() + await asset.delete() + + assert asset_id == "asset_uploaded_01" + assert server.state.delete_count == 1 + assert asset.id is None + + +async def test_async_delete_uncommitted_asset_raises(server) -> None: + async with AsyncComfy(server.base_url) as client: + asset = client.assets.from_bytes(b"not-uploaded", filename="photo.png") + with pytest.raises(RuntimeError, match="uncommitted"): + await asset.delete() + + assert server.state.delete_count == 0 From 1317e5f5a67bba83663f22cb1e6cc486b5c0abef Mon Sep 17 00:00:00 2001 From: jab416171 Date: Fri, 7 Aug 2026 17:12:04 -0600 Subject: [PATCH 2/3] address coderabbit PR comments --- spec/openapi.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/spec/openapi.yaml b/spec/openapi.yaml index 8a5f4c1..5809804 100644 --- a/spec/openapi.yaml +++ b/spec/openapi.yaml @@ -233,6 +233,25 @@ paths: $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/UpstreamError' + delete: + operationId: deleteAsset + tags: + - assets + summary: Delete an asset + description: Removes the asset record and its content. + parameters: + - $ref: '#/components/parameters/AssetId' + responses: + '204': + description: Asset deleted. + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/UpstreamError' /api/v2/assets/{id}/content: get: operationId: getAssetContent From 7de4ad0e2125f36ba7a21c700eaeebf4eb00da9d Mon Sep 17 00:00:00 2001 From: jab416171 Date: Fri, 7 Aug 2026 17:19:36 -0600 Subject: [PATCH 3/3] address coderabbit PR comments --- tests/conftest.py | 8 ++++++++ tests/test_assets.py | 5 +++++ tests/test_async.py | 5 +++++ 3 files changed, 18 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 2156db0..0837998 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -59,6 +59,7 @@ class ServerState: from_hash_count: int = 0 head_count: int = 0 delete_count: int = 0 + deleted_assets: set[str] = field(default_factory=set) job_poll_count: int = 0 events_connect_count: int = 0 submit_count: int = 0 @@ -183,6 +184,7 @@ def do_DELETE(self) -> None: m = re.match(r"/api/v2/assets/([^/]+)$", self.path) if m: state.delete_count += 1 + state.deleted_assets.add(m.group(1)) self.send_response(204) self.end_headers() return @@ -196,6 +198,9 @@ def do_GET(self) -> None: m = re.match(r"/api/v2/assets/([^/]+)/content$", self.path) if m: + if m.group(1) in state.deleted_assets: + self._err(404, "not_found") + return if state.redirect_content_to: self._redirect(state.redirect_content_to) else: @@ -203,6 +208,9 @@ def do_GET(self) -> None: return m = re.match(r"/api/v2/assets/([^/]+)$", self.path) if m: + if m.group(1) in state.deleted_assets: + self._err(404, "not_found") + return self._json(200, _asset_json(m.group(1), state.server_hash, False, 33)) return m = re.match(r"/api/v2/jobs/([^/]+)/events$", self.path) diff --git a/tests/test_assets.py b/tests/test_assets.py index e9f00bb..ea2ce8e 100644 --- a/tests/test_assets.py +++ b/tests/test_assets.py @@ -6,6 +6,7 @@ import pytest +from comfy_low.errors import NotFound from comfy_sdk import Comfy, HashMismatch @@ -107,6 +108,8 @@ def test_hash_mismatch_surfaced_without_blind_retry(server, tmp_path) -> None: def test_delete_asset_by_id(server) -> None: with Comfy(server.base_url) as client: client.assets.delete("asset_uuid_01") + with pytest.raises(NotFound): + client.assets.get("asset_uuid_01") assert server.state.delete_count == 1 @@ -118,6 +121,8 @@ def test_delete_asset_on_asset_instance(server) -> None: asset.commit() asset_id = asset.id asset.delete() + with pytest.raises(NotFound): + client.assets.get(asset_id) assert asset_id == "asset_uploaded_01" assert server.state.delete_count == 1 diff --git a/tests/test_async.py b/tests/test_async.py index 1cbe1d0..8839227 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -4,6 +4,7 @@ import pytest +from comfy_low.errors import NotFound from comfy_sdk import AsyncComfy, MissingAsset, Progress, StatusChange @@ -152,6 +153,8 @@ async def test_async_queue_full_retries_with_retry_after(server) -> None: async def test_async_delete_asset_by_id(server) -> None: async with AsyncComfy(server.base_url) as client: await client.assets.delete("asset_uuid_01") + with pytest.raises(NotFound): + await client.assets.get("asset_uuid_01") assert server.state.delete_count == 1 @@ -162,6 +165,8 @@ async def test_async_delete_asset_on_asset_instance(server) -> None: asset = client.assets.from_bytes(data, filename="photo.png") asset_id = await asset.commit() await asset.delete() + with pytest.raises(NotFound): + await client.assets.get(asset_id) assert asset_id == "asset_uploaded_01" assert server.state.delete_count == 1