Skip to content

Commit 4ab0d98

Browse files
JasonW404jason
andauthored
feat: add PUT /{file_id}/replace endpoint for clearing file tags (#479)
Add new API endpoint to fully replace (not merge) file annotation tags. - Empty tag list now clears all tags (partial update kept them) - Supports both simplified and full tag formats with auto-conversion - Addresses bug where tags couldn't be deleted via empty list Co-authored-by: jason <jason@example.com>
1 parent 3306b1c commit 4ab0d98

2 files changed

Lines changed: 156 additions & 0 deletions

File tree

  • runtime/datamate-python/app/module

runtime/datamate-python/app/module/annotation/interface/task.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,3 +321,81 @@ async def update_file_tags(
321321
message="标签更新成功",
322322
data=response_data
323323
)
324+
325+
326+
@router.put(
327+
"/{file_id}/replace",
328+
response_model=StandardResponse[UpdateFileTagsResponse],
329+
)
330+
async def replace_file_tags(
331+
request: UpdateFileTagsRequest,
332+
file_id: str = Path(..., description="文件ID"),
333+
db: AsyncSession = Depends(get_db)
334+
):
335+
"""
336+
Replace File Tags (Full Replacement with Auto Format Conversion)
337+
338+
完全替换文件的标签列表。与部分更新不同,此方法会:
339+
- 空列表会清空所有标签
340+
- 新标签列表完全替换原有标签(不合并)
341+
342+
支持两种标签格式:
343+
1. 简化格式:
344+
[{"from_name": "label", "to_name": "image", "values": ["cat", "dog"]}]
345+
346+
2. 完整格式:
347+
[{"id": "...", "from_name": "label", "to_name": "image", "type": "choices",
348+
"value": {"choices": ["cat", "dog"]}}]
349+
350+
系统会自动根据数据集关联的模板将简化格式转换为完整格式。
351+
"""
352+
service = DatasetManagementService(db)
353+
354+
result = await db.execute(
355+
select(DatasetFiles).where(DatasetFiles.id == file_id)
356+
)
357+
file_record = result.scalar_one_or_none()
358+
359+
if not file_record:
360+
raise HTTPException(status_code=404, detail=f"File not found: {file_id}")
361+
362+
dataset_id = str(file_record.dataset_id)
363+
364+
mapping_service = DatasetMappingService(db)
365+
template_id = await mapping_service.get_template_id_by_dataset_id(dataset_id)
366+
367+
if template_id:
368+
logger.info(f"Found template {template_id} for dataset {dataset_id}, will auto-convert tag format")
369+
else:
370+
logger.warning(f"No template found for dataset {dataset_id}, tags must be in full format")
371+
372+
success, error_msg, updated_at = await service.replace_file_tags(
373+
file_id=file_id,
374+
new_tags=request.tags,
375+
template_id=template_id
376+
)
377+
378+
if not success:
379+
if "not found" in (error_msg or "").lower():
380+
raise HTTPException(status_code=404, detail=error_msg)
381+
raise HTTPException(status_code=500, detail=error_msg or "替换标签失败")
382+
383+
result = await db.execute(
384+
select(DatasetFiles).where(DatasetFiles.id == file_id)
385+
)
386+
file_record = result.scalar_one_or_none()
387+
388+
if not file_record:
389+
raise HTTPException(status_code=404, detail=f"File not found: {file_id}")
390+
391+
response_data = UpdateFileTagsResponse(
392+
fileId=file_id,
393+
tags=file_record.tags or [],
394+
tagsUpdatedAt=updated_at or datetime.now()
395+
)
396+
397+
return StandardResponse(
398+
code="0",
399+
message="标签替换成功",
400+
data=response_data
401+
)

runtime/datamate-python/app/module/dataset/service/service.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,84 @@ def _index_tag(idx: int, tag: Dict[str, Any]) -> None:
470470
await self.db.rollback()
471471
return False, str(e), None
472472

473+
async def replace_file_tags(
474+
self,
475+
file_id: str,
476+
new_tags: List[Dict[str, Any]],
477+
template_id: Optional[str] = None
478+
) -> tuple[bool, Optional[str], Optional[datetime]]:
479+
"""
480+
完全替换文件标签(非部分更新)
481+
482+
与部分更新不同,此方法会完全替换标签列表:
483+
- 空列表会清空所有标签
484+
- 新标签列表会完全替换原有标签(不合并)
485+
486+
如果提供了 template_id,会自动将简化格式的标签转换为完整格式。
487+
488+
Args:
489+
file_id: 文件ID
490+
new_tags: 新的标签列表(完全替换),可以是简化格式或完整格式,空列表会清空所有标签
491+
template_id: 可选的模板ID,用于格式转换
492+
493+
Returns:
494+
(成功标志, 错误信息, 更新时间)
495+
"""
496+
try:
497+
logger.info(f"Replacing tags for file: {file_id}, new_tags count: {len(new_tags)}")
498+
499+
result = await self.db.execute(
500+
select(DatasetFiles).where(DatasetFiles.id == file_id)
501+
)
502+
file_record = result.scalar_one_or_none()
503+
504+
if not file_record:
505+
logger.error(f"File not found: {file_id}")
506+
return False, f"File not found: {file_id}", None
507+
508+
processed_tags = new_tags
509+
if template_id and new_tags:
510+
logger.debug(f"Converting tags using template: {template_id}")
511+
512+
try:
513+
from app.db.models import AnnotationTemplate
514+
template_result = await self.db.execute(
515+
select(AnnotationTemplate).where(
516+
AnnotationTemplate.id == template_id,
517+
AnnotationTemplate.deleted_at.is_(None)
518+
)
519+
)
520+
template = template_result.scalar_one_or_none()
521+
522+
if not template:
523+
logger.warning(f"Template {template_id} not found, skipping conversion")
524+
else:
525+
from app.module.annotation.utils import create_converter_from_template_config
526+
527+
converter = create_converter_from_template_config(template.configuration) # type: ignore
528+
processed_tags = converter.convert_if_needed(new_tags)
529+
530+
logger.info(f"Converted {len(new_tags)} tags to full format")
531+
532+
except Exception as e:
533+
logger.error(f"Failed to convert tags using template: {e}")
534+
logger.warning("Continuing with original tag format")
535+
536+
update_time = datetime.utcnow()
537+
file_record.tags = processed_tags # type: ignore
538+
file_record.tags_updated_at = update_time # type: ignore
539+
540+
await self.db.commit()
541+
await self.db.refresh(file_record)
542+
543+
logger.info(f"Successfully replaced tags for file: {file_id}, new tags count: {len(processed_tags)}")
544+
return True, None, update_time
545+
546+
except Exception as e:
547+
logger.error(f"Failed to replace tags for file {file_id}: {e}")
548+
await self.db.rollback()
549+
return False, str(e), None
550+
473551
@staticmethod
474552
async def _get_or_create_dataset_directory(dataset: Dataset) -> str:
475553
"""Get or create dataset directory"""

0 commit comments

Comments
 (0)