Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions spec/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions src/comfy_low/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,))

Comment thread
coderabbitai[bot] marked this conversation as resolved.
@contextmanager
def get_asset_content(
self,
Expand Down Expand Up @@ -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,
Expand Down
26 changes: 26 additions & 0 deletions src/comfy_sdk/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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`."""
Expand Down Expand Up @@ -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")
Expand Down
22 changes: 22 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ class ServerState:
upload_count: int = 0
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
Expand Down Expand Up @@ -174,6 +176,20 @@ 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
state.deleted_assets.add(m.group(1))
self.send_response(204)
self.end_headers()
return
Comment thread
coderabbitai[bot] marked this conversation as resolved.
self._err(404, "not_found")

# -- GET --
def do_GET(self) -> None:
if not self._auth_ok():
Expand All @@ -182,13 +198,19 @@ 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:
self._serve_content()
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)
Expand Down
34 changes: 34 additions & 0 deletions tests/test_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import pytest

from comfy_low.errors import NotFound
from comfy_sdk import Comfy, HashMismatch


Expand Down Expand Up @@ -102,3 +103,36 @@ 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")
with pytest.raises(NotFound):
client.assets.get("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()
with pytest.raises(NotFound):
client.assets.get(asset_id)

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
33 changes: 33 additions & 0 deletions tests/test_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import pytest

from comfy_low.errors import NotFound
from comfy_sdk import AsyncComfy, MissingAsset, Progress, StatusChange


Expand Down Expand Up @@ -147,3 +148,35 @@ 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")
with pytest.raises(NotFound):
await client.assets.get("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()
with pytest.raises(NotFound):
await client.assets.get(asset_id)

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
Loading