From 63f9f1b11bb06cc93a8a35391de575373efa73c1 Mon Sep 17 00:00:00 2001 From: Jedrzej Kosinski Date: Fri, 16 Jan 2026 00:50:13 -0800 Subject: [PATCH] Finish @ROUTES.delete(f"/api/assets/{{id:{UUID_RE}}}/tags") --- app/assets/api/routes.py | 30 +++++++++++++++++++++++++ app/assets/api/schemas_in.py | 4 ++++ app/assets/api/schemas_out.py | 7 ++++++ app/assets/database/queries.py | 41 ++++++++++++++++++++++++++++++++++ app/assets/manager.py | 23 +++++++++++++++++++ 5 files changed, 105 insertions(+) diff --git a/app/assets/api/routes.py b/app/assets/api/routes.py index 20e796b4d..916b85f6c 100644 --- a/app/assets/api/routes.py +++ b/app/assets/api/routes.py @@ -462,3 +462,33 @@ async def add_asset_tags(request: web.Request) -> web.Response: return _error_response(500, "INTERNAL", "Unexpected server error.") return web.json_response(result.model_dump(mode="json"), status=200) + + +@ROUTES.delete(f"/api/assets/{{id:{UUID_RE}}}/tags") +async def delete_asset_tags(request: web.Request) -> web.Response: + asset_info_id = str(uuid.UUID(request.match_info["id"])) + try: + payload = await request.json() + data = schemas_in.TagsRemove.model_validate(payload) + except ValidationError as ve: + return _error_response(400, "INVALID_BODY", "Invalid JSON body for tags remove.", {"errors": ve.errors()}) + except Exception: + return _error_response(400, "INVALID_JSON", "Request body must be valid JSON.") + + try: + result = manager.remove_tags_from_asset( + asset_info_id=asset_info_id, + tags=data.tags, + owner_id=USER_MANAGER.get_request_user_id(request), + ) + except ValueError as ve: + return _error_response(404, "ASSET_NOT_FOUND", str(ve), {"id": asset_info_id}) + except Exception: + logging.exception( + "remove_tags_from_asset failed for asset_info_id=%s, owner_id=%s", + asset_info_id, + USER_MANAGER.get_request_user_id(request), + ) + return _error_response(500, "INTERNAL", "Unexpected server error.") + + return web.json_response(result.model_dump(mode="json"), status=200) diff --git a/app/assets/api/schemas_in.py b/app/assets/api/schemas_in.py index 02908120d..2145127a0 100644 --- a/app/assets/api/schemas_in.py +++ b/app/assets/api/schemas_in.py @@ -154,6 +154,10 @@ class TagsAdd(BaseModel): return deduplicated +class TagsRemove(TagsAdd): + pass + + class UploadAssetSpec(BaseModel): """Upload Asset operation. - tags: ordered; first is root ('models'|'input'|'output'); diff --git a/app/assets/api/schemas_out.py b/app/assets/api/schemas_out.py index 3c1127fae..b6fb3da0c 100644 --- a/app/assets/api/schemas_out.py +++ b/app/assets/api/schemas_out.py @@ -84,3 +84,10 @@ class TagsAdd(BaseModel): added: list[str] = Field(default_factory=list) already_present: list[str] = Field(default_factory=list) total_tags: list[str] = Field(default_factory=list) + + +class TagsRemove(BaseModel): + model_config = ConfigDict(str_strip_whitespace=True) + removed: list[str] = Field(default_factory=list) + not_present: list[str] = Field(default_factory=list) + total_tags: list[str] = Field(default_factory=list) diff --git a/app/assets/database/queries.py b/app/assets/database/queries.py index d51a02fe2..441a4e14c 100644 --- a/app/assets/database/queries.py +++ b/app/assets/database/queries.py @@ -898,6 +898,47 @@ def add_tags_to_asset_info( } +def remove_tags_from_asset_info( + session: Session, + *, + asset_info_id: str, + tags: Sequence[str], +) -> dict: + info = session.get(AssetInfo, asset_info_id) + if not info: + raise ValueError(f"AssetInfo {asset_info_id} not found") + + norm = normalize_tags(tags) + if not norm: + total = get_asset_tags(session, asset_info_id=asset_info_id) + return {"removed": [], "not_present": [], "total_tags": total} + + existing = { + tag_name + for (tag_name,) in ( + session.execute( + sa.select(AssetInfoTag.tag_name).where(AssetInfoTag.asset_info_id == asset_info_id) + ) + ).all() + } + + to_remove = sorted(set(t for t in norm if t in existing)) + not_present = sorted(set(t for t in norm if t not in existing)) + + if to_remove: + session.execute( + delete(AssetInfoTag) + .where( + AssetInfoTag.asset_info_id == asset_info_id, + AssetInfoTag.tag_name.in_(to_remove), + ) + ) + session.flush() + + total = get_asset_tags(session, asset_info_id=asset_info_id) + return {"removed": to_remove, "not_present": not_present, "total_tags": total} + + def remove_missing_tag_for_asset_id( session: Session, *, diff --git a/app/assets/manager.py b/app/assets/manager.py index 666f940bc..b1ed35815 100644 --- a/app/assets/manager.py +++ b/app/assets/manager.py @@ -21,6 +21,7 @@ from app.assets.database.queries import ( list_tags_with_usage, get_asset_tags, add_tags_to_asset_info, + remove_tags_from_asset_info, pick_best_live_path, ingest_fs_asset, set_asset_info_preview, @@ -460,6 +461,28 @@ def add_tags_to_asset( return schemas_out.TagsAdd(**data) +def remove_tags_from_asset( + *, + asset_info_id: str, + tags: list[str], + owner_id: str = "", +) -> schemas_out.TagsRemove: + with create_session() as session: + info_row = get_asset_info_by_id(session, asset_info_id=asset_info_id) + if not info_row: + raise ValueError(f"AssetInfo {asset_info_id} not found") + if info_row.owner_id and info_row.owner_id != owner_id: + raise PermissionError("not owner") + + data = remove_tags_from_asset_info( + session, + asset_info_id=asset_info_id, + tags=tags, + ) + session.commit() + return schemas_out.TagsRemove(**data) + + def list_tags( prefix: str | None = None, limit: int = 100,