annofabcli 1.100.5__py3-none-any.whl → 1.101.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- annofabcli/annotation/change_annotation_attributes.py +3 -2
- annofabcli/annotation/change_annotation_properties.py +8 -7
- annofabcli/annotation/copy_annotation.py +4 -4
- annofabcli/annotation/delete_annotation.py +254 -29
- annofabcli/annotation/dump_annotation.py +14 -7
- annofabcli/annotation/import_annotation.py +304 -227
- annofabcli/annotation/restore_annotation.py +7 -7
- annofabcli/annotation_specs/list_annotation_specs_attribute.py +1 -1
- annofabcli/annotation_specs/list_annotation_specs_choice.py +1 -1
- annofabcli/annotation_specs/list_annotation_specs_label.py +1 -1
- annofabcli/annotation_specs/list_annotation_specs_label_attribute.py +1 -1
- annofabcli/comment/delete_comment.py +7 -5
- annofabcli/comment/put_comment.py +1 -1
- annofabcli/comment/put_comment_simply.py +7 -5
- annofabcli/common/cli.py +10 -10
- annofabcli/common/download.py +28 -29
- annofabcli/common/image.py +4 -2
- annofabcli/input_data/delete_input_data.py +4 -4
- annofabcli/input_data/update_metadata_of_input_data.py +1 -1
- annofabcli/instruction/upload_instruction.py +2 -2
- annofabcli/statistics/visualize_statistics.py +1 -7
- annofabcli/supplementary/delete_supplementary_data.py +8 -4
- {annofabcli-1.100.5.dist-info → annofabcli-1.101.0.dist-info}/METADATA +3 -2
- {annofabcli-1.100.5.dist-info → annofabcli-1.101.0.dist-info}/RECORD +27 -28
- annofabcli/__version__.py +0 -1
- {annofabcli-1.100.5.dist-info → annofabcli-1.101.0.dist-info}/WHEEL +0 -0
- {annofabcli-1.100.5.dist-info → annofabcli-1.101.0.dist-info}/entry_points.txt +0 -0
- {annofabcli-1.100.5.dist-info → annofabcli-1.101.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import functools
|
|
5
|
+
import json
|
|
5
6
|
import logging
|
|
6
7
|
import multiprocessing
|
|
7
8
|
import sys
|
|
@@ -345,7 +346,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
345
346
|
argument_parser.add_project_id()
|
|
346
347
|
argument_parser.add_task_id()
|
|
347
348
|
|
|
348
|
-
EXAMPLE_ANNOTATION_QUERY =
|
|
349
|
+
EXAMPLE_ANNOTATION_QUERY = {"label": "car", "attributes": {"occluded": True}} # noqa: N806
|
|
349
350
|
parser.add_argument(
|
|
350
351
|
"-aq",
|
|
351
352
|
"--annotation_query",
|
|
@@ -353,7 +354,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
353
354
|
required=True,
|
|
354
355
|
help="変更対象のアノテーションを検索する条件をJSON形式で指定します。"
|
|
355
356
|
"``file://`` を先頭に付けると、JSON形式のファイルを指定できます。"
|
|
356
|
-
f"(ex): ``{EXAMPLE_ANNOTATION_QUERY}``",
|
|
357
|
+
f"(ex): ``{json.dumps(EXAMPLE_ANNOTATION_QUERY)}``",
|
|
357
358
|
)
|
|
358
359
|
|
|
359
360
|
EXAMPLE_ATTRIBUTES = '{"occluded": false}' # noqa: N806
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import functools
|
|
5
|
+
import json
|
|
5
6
|
import logging
|
|
6
7
|
import multiprocessing
|
|
7
8
|
import sys
|
|
@@ -156,7 +157,7 @@ class ChangePropertiesOfAnnotationMain(CommandLineWithConfirm):
|
|
|
156
157
|
return False
|
|
157
158
|
|
|
158
159
|
logger.debug(
|
|
159
|
-
f"{logger_prefix}task_id={task_id}, phase={dict_task['phase']}, status={dict_task['status']}, "
|
|
160
|
+
f"{logger_prefix}task_id='{task_id}', phase={dict_task['phase']}, status={dict_task['status']}, "
|
|
160
161
|
f"updated_datetime={dict_task['updated_datetime']}"
|
|
161
162
|
)
|
|
162
163
|
|
|
@@ -202,10 +203,10 @@ class ChangePropertiesOfAnnotationMain(CommandLineWithConfirm):
|
|
|
202
203
|
|
|
203
204
|
try:
|
|
204
205
|
self.change_annotation_properties(task_id, annotation_list, properties)
|
|
205
|
-
logger.info(f"{logger_prefix}task_id={task_id}
|
|
206
|
+
logger.info(f"{logger_prefix}task_id='{task_id}' :: アノテーションのプロパティを変更しました。")
|
|
206
207
|
return True # noqa: TRY300
|
|
207
208
|
except Exception: # pylint: disable=broad-except
|
|
208
|
-
logger.warning(f"task_id={task_id}
|
|
209
|
+
logger.warning(f"task_id='{task_id}' :: アノテーションのプロパティの変更に失敗しました。", exc_info=True)
|
|
209
210
|
return False
|
|
210
211
|
finally:
|
|
211
212
|
if changed_operator:
|
|
@@ -234,7 +235,7 @@ class ChangePropertiesOfAnnotationMain(CommandLineWithConfirm):
|
|
|
234
235
|
task_index=task_index,
|
|
235
236
|
)
|
|
236
237
|
except Exception: # pylint: disable=broad-except
|
|
237
|
-
logger.warning(f"task_id={task_id}
|
|
238
|
+
logger.warning(f"task_id='{task_id}' :: アノテーションのプロパティの変更に失敗しました。", exc_info=True)
|
|
238
239
|
return False
|
|
239
240
|
|
|
240
241
|
def change_annotation_properties_task_list( # noqa: ANN201
|
|
@@ -277,7 +278,7 @@ class ChangePropertiesOfAnnotationMain(CommandLineWithConfirm):
|
|
|
277
278
|
if result:
|
|
278
279
|
success_count += 1
|
|
279
280
|
except Exception: # pylint: disable=broad-except
|
|
280
|
-
logger.warning(f"task_id={task_id}
|
|
281
|
+
logger.warning(f"task_id='{task_id}' :: アノテーションのプロパティの変更に失敗しました。", exc_info=True)
|
|
281
282
|
continue
|
|
282
283
|
|
|
283
284
|
logger.info(f"{success_count} / {len(task_id_list)} 件のタスクに対してアノテーションのプロパティを変更しました。")
|
|
@@ -359,14 +360,14 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
359
360
|
argument_parser.add_project_id()
|
|
360
361
|
argument_parser.add_task_id()
|
|
361
362
|
|
|
362
|
-
EXAMPLE_ANNOTATION_QUERY =
|
|
363
|
+
EXAMPLE_ANNOTATION_QUERY = {"label": "car", "attributes": {"occluded": True}} # noqa: N806
|
|
363
364
|
|
|
364
365
|
parser.add_argument(
|
|
365
366
|
"-aq",
|
|
366
367
|
"--annotation_query",
|
|
367
368
|
type=str,
|
|
368
369
|
required=False,
|
|
369
|
-
help=f"変更対象のアノテーションを検索する条件をJSON形式で指定します。(ex): ``{EXAMPLE_ANNOTATION_QUERY}``",
|
|
370
|
+
help=f"変更対象のアノテーションを検索する条件をJSON形式で指定します。(ex): ``{json.dumps(EXAMPLE_ANNOTATION_QUERY)}``",
|
|
370
371
|
)
|
|
371
372
|
|
|
372
373
|
EXAMPLE_PROPERTIES = '{"is_protected": true}' # noqa: N806
|
|
@@ -48,11 +48,11 @@ class CopyTarget(CopyTargetMixin, ABC):
|
|
|
48
48
|
@dataclass(frozen=True)
|
|
49
49
|
class CopyTargetByTask(CopyTarget):
|
|
50
50
|
@property
|
|
51
|
-
def src(self)
|
|
51
|
+
def src(self) -> str:
|
|
52
52
|
return f"{self.src_task_id}"
|
|
53
53
|
|
|
54
54
|
@property
|
|
55
|
-
def dest(self)
|
|
55
|
+
def dest(self) -> str:
|
|
56
56
|
return f"{self.dest_task_id}"
|
|
57
57
|
|
|
58
58
|
|
|
@@ -62,11 +62,11 @@ class CopyTargetByInputData(CopyTarget):
|
|
|
62
62
|
dest_input_data_id: str
|
|
63
63
|
|
|
64
64
|
@property
|
|
65
|
-
def src(self)
|
|
65
|
+
def src(self) -> str:
|
|
66
66
|
return f"{self.src_task_id}/{self.src_input_data_id}"
|
|
67
67
|
|
|
68
68
|
@property
|
|
69
|
-
def dest(self)
|
|
69
|
+
def dest(self) -> str:
|
|
70
70
|
return f"{self.dest_task_id}/{self.dest_input_data_id}"
|
|
71
71
|
|
|
72
72
|
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import json
|
|
4
5
|
import logging
|
|
5
6
|
import sys
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
from dataclasses import dataclass
|
|
6
9
|
from pathlib import Path
|
|
7
10
|
from typing import Any, Optional
|
|
8
11
|
|
|
9
12
|
import annofabapi
|
|
13
|
+
import pandas
|
|
10
14
|
import requests
|
|
11
15
|
from annofabapi.dataclass.task import Task
|
|
12
16
|
from annofabapi.models import ProjectMemberRole, TaskStatus
|
|
@@ -27,6 +31,17 @@ from annofabcli.common.facade import AnnofabApiFacade
|
|
|
27
31
|
logger = logging.getLogger(__name__)
|
|
28
32
|
|
|
29
33
|
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class DeletedAnnotationInfo:
|
|
36
|
+
"""
|
|
37
|
+
削除対象のアノテーション情報
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
task_id: str
|
|
41
|
+
input_data_id: str
|
|
42
|
+
annotation_id: str
|
|
43
|
+
|
|
44
|
+
|
|
30
45
|
class DeleteAnnotationMain(CommandLineWithConfirm):
|
|
31
46
|
"""アノテーション削除処理用のクラス
|
|
32
47
|
|
|
@@ -49,7 +64,7 @@ class DeleteAnnotationMain(CommandLineWithConfirm):
|
|
|
49
64
|
self.project_id = project_id
|
|
50
65
|
self.dump_annotation_obj = DumpAnnotationMain(service, project_id)
|
|
51
66
|
|
|
52
|
-
def delete_annotation_list(self, annotation_list: list[dict[str, Any]])
|
|
67
|
+
def delete_annotation_list(self, annotation_list: list[dict[str, Any]]) -> None:
|
|
53
68
|
"""
|
|
54
69
|
アノテーション一覧を削除する。
|
|
55
70
|
|
|
@@ -108,19 +123,22 @@ class DeleteAnnotationMain(CommandLineWithConfirm):
|
|
|
108
123
|
"""
|
|
109
124
|
dict_task = self.service.wrapper.get_task_or_none(self.project_id, task_id)
|
|
110
125
|
if dict_task is None:
|
|
111
|
-
logger.warning(f"task_id
|
|
126
|
+
logger.warning(f"task_id='{task_id}'であるタスクは存在しません。")
|
|
112
127
|
return
|
|
113
128
|
|
|
114
129
|
task: Task = Task.from_dict(dict_task)
|
|
115
|
-
logger.info(f"task_id={task.task_id}, phase={task.phase.value}, status={task.status.value}, updated_datetime={task.updated_datetime}")
|
|
130
|
+
logger.info(f"task_id='{task.task_id}', phase='{task.phase.value}', status='{task.status.value}', updated_datetime='{task.updated_datetime}'")
|
|
116
131
|
|
|
117
132
|
if task.status == TaskStatus.WORKING:
|
|
118
|
-
logger.
|
|
133
|
+
logger.info(f"task_id='{task_id}' :: タスクが作業中状態のため、スキップします。")
|
|
119
134
|
return
|
|
120
135
|
|
|
121
136
|
if not self.is_force: # noqa: SIM102
|
|
122
137
|
if task.status == TaskStatus.COMPLETE:
|
|
123
|
-
logger.
|
|
138
|
+
logger.info(
|
|
139
|
+
f"task_id='{task_id}' :: タスクが完了状態のため、スキップします。"
|
|
140
|
+
f"完了状態のタスクのアノテーションを削除するには、`--force`オプションを指定してください。"
|
|
141
|
+
)
|
|
124
142
|
return
|
|
125
143
|
|
|
126
144
|
annotation_list = self.get_annotation_list_for_task(task_id, annotation_query=annotation_query)
|
|
@@ -137,16 +155,16 @@ class DeleteAnnotationMain(CommandLineWithConfirm):
|
|
|
137
155
|
|
|
138
156
|
try:
|
|
139
157
|
self.delete_annotation_list(annotation_list=annotation_list)
|
|
140
|
-
logger.info(f"task_id={task_id}
|
|
158
|
+
logger.info(f"task_id='{task_id}' :: アノテーションを削除しました。")
|
|
141
159
|
except requests.HTTPError:
|
|
142
|
-
logger.warning(f"task_id={task_id}
|
|
160
|
+
logger.warning(f"task_id='{task_id}' :: アノテーションの削除に失敗しました。", exc_info=True)
|
|
143
161
|
|
|
144
|
-
def delete_annotation_for_task_list(
|
|
162
|
+
def delete_annotation_for_task_list(
|
|
145
163
|
self,
|
|
146
164
|
task_id_list: list[str],
|
|
147
165
|
annotation_query: Optional[AnnotationQueryForAPI] = None,
|
|
148
166
|
backup_dir: Optional[Path] = None,
|
|
149
|
-
):
|
|
167
|
+
) -> None:
|
|
150
168
|
project_title = self.facade.get_project_title(self.project_id)
|
|
151
169
|
logger.info(f"プロジェクト'{project_title}'に対して、タスク{len(task_id_list)} 件のアノテーションを削除します。")
|
|
152
170
|
|
|
@@ -161,6 +179,160 @@ class DeleteAnnotationMain(CommandLineWithConfirm):
|
|
|
161
179
|
backup_dir=backup_dir,
|
|
162
180
|
)
|
|
163
181
|
|
|
182
|
+
def delete_annotation_by_annotation_ids(
|
|
183
|
+
self,
|
|
184
|
+
editor_annotation: dict[str, Any],
|
|
185
|
+
annotation_ids: set[str],
|
|
186
|
+
) -> tuple[int, int]:
|
|
187
|
+
"""
|
|
188
|
+
指定してフレームに対して、annotation_idのリストで指定されたアノテーションを削除する。
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
削除したアノテーションの件数
|
|
192
|
+
削除しなかったアノテーションの件数
|
|
193
|
+
"""
|
|
194
|
+
if not annotation_ids:
|
|
195
|
+
raise ValueError("`annotation_ids` に少なくとも1件のIDを指定してください。")
|
|
196
|
+
|
|
197
|
+
task_id = editor_annotation["task_id"]
|
|
198
|
+
input_data_id = editor_annotation["input_data_id"]
|
|
199
|
+
if len(editor_annotation["details"]) == 0:
|
|
200
|
+
logger.warning(
|
|
201
|
+
f"task_id='{task_id}', input_data_id='{input_data_id}' にはアノテーションが存在しません。以下のアノテーションの削除をスキップします。 :: " # noqa: E501
|
|
202
|
+
f"annotation_ids={annotation_ids}"
|
|
203
|
+
)
|
|
204
|
+
return 0, len(annotation_ids)
|
|
205
|
+
|
|
206
|
+
# annotation_idでフィルタ
|
|
207
|
+
filtered_details = [e for e in editor_annotation["details"] if e["annotation_id"] in annotation_ids]
|
|
208
|
+
existent_annotation_ids = {detail["annotation_id"] for detail in filtered_details}
|
|
209
|
+
nonexistent_annotation_ids = annotation_ids - existent_annotation_ids
|
|
210
|
+
|
|
211
|
+
if len(nonexistent_annotation_ids) > 0:
|
|
212
|
+
logger.warning(
|
|
213
|
+
f"次のアノテーションは存在しないので、削除できません。 :: task_id='{task_id}', input_data_id='{input_data_id}', annotation_id='{nonexistent_annotation_ids}'" # noqa: E501
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if len(filtered_details) == 0:
|
|
217
|
+
logger.info(f"task_id='{task_id}', input_data_id='{input_data_id}' には削除対象のアノテーションが存在しないので、スキップします。")
|
|
218
|
+
return 0, len(annotation_ids)
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
|
|
222
|
+
def _to_request_body_elm(detail: dict[str, Any]) -> dict[str, Any]:
|
|
223
|
+
return {
|
|
224
|
+
"project_id": self.project_id,
|
|
225
|
+
"task_id": task_id,
|
|
226
|
+
"input_data_id": input_data_id,
|
|
227
|
+
"updated_datetime": editor_annotation["updated_datetime"],
|
|
228
|
+
"annotation_id": detail["annotation_id"],
|
|
229
|
+
"_type": "Delete",
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
request_body = [_to_request_body_elm(detail) for detail in filtered_details]
|
|
233
|
+
# APIを呼び出してアノテーションを削除
|
|
234
|
+
self.service.api.batch_update_annotations(self.project_id, request_body=request_body, query_params={"v": "2"})
|
|
235
|
+
|
|
236
|
+
logger.info(f"task_id='{task_id}', input_data_id='{input_data_id}' に含まれるアノテーション {len(filtered_details)} 件を削除しました。")
|
|
237
|
+
|
|
238
|
+
return len(filtered_details), len(nonexistent_annotation_ids)
|
|
239
|
+
|
|
240
|
+
except requests.HTTPError:
|
|
241
|
+
logger.warning(f"task_id='{task_id}', input_data_id='{input_data_id}' :: アノテーションの削除に失敗しました。", exc_info=True)
|
|
242
|
+
# `batchUpdateAnnotations` APIでエラーになった場合、途中までは削除されるので、`len(annotation_ids)` 件削除に失敗したとは限らない。
|
|
243
|
+
# そのため、再度アノテーション情報を取得して、削除できたアノテーション数と削除できなかったアノテーション数を取得する。
|
|
244
|
+
new_editor_annotation, _ = self.service.api.get_editor_annotation(
|
|
245
|
+
self.project_id, task_id=task_id, input_data_id=input_data_id, query_params={"v": "2"}
|
|
246
|
+
)
|
|
247
|
+
new_annotation_ids = {e["annotation_id"] for e in new_editor_annotation["details"]}
|
|
248
|
+
deleted_annotation_count = len(existent_annotation_ids - new_annotation_ids)
|
|
249
|
+
failed_to_delete_annotation_count = len(existent_annotation_ids) - deleted_annotation_count
|
|
250
|
+
return deleted_annotation_count, failed_to_delete_annotation_count + len(nonexistent_annotation_ids)
|
|
251
|
+
|
|
252
|
+
def delete_annotation_by_id_list(
|
|
253
|
+
self,
|
|
254
|
+
annotation_list: list[DeletedAnnotationInfo],
|
|
255
|
+
backup_dir: Optional[Path] = None,
|
|
256
|
+
) -> None:
|
|
257
|
+
"""
|
|
258
|
+
task_id, input_data_id, annotation_id のリストで指定されたアノテーションのみ削除する
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
annotation_list: 削除対象のアノテーションlist
|
|
262
|
+
backup_dir: バックアップディレクトリ
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
# task_id, input_data_idごとにまとめる
|
|
266
|
+
grouped: dict[str, dict[str, list[str]]] = defaultdict(lambda: defaultdict(list))
|
|
267
|
+
for item in annotation_list:
|
|
268
|
+
grouped[item.task_id][item.input_data_id].append(item.annotation_id)
|
|
269
|
+
|
|
270
|
+
total = len(annotation_list)
|
|
271
|
+
|
|
272
|
+
deleted_annotation_count = 0
|
|
273
|
+
failed_to_delete_annotation_count = 0
|
|
274
|
+
|
|
275
|
+
task_count = len(grouped)
|
|
276
|
+
deleted_task_count = 0
|
|
277
|
+
|
|
278
|
+
logger.info(f"{task_count} 件のタスクに含まれるアノテーションを削除します。")
|
|
279
|
+
|
|
280
|
+
for task_id, sub_grouped in grouped.items():
|
|
281
|
+
annotation_count = sum(len(v) for v in sub_grouped.values())
|
|
282
|
+
task = self.service.wrapper.get_task_or_none(self.project_id, task_id)
|
|
283
|
+
if task is None:
|
|
284
|
+
logger.warning(f"task_id='{task_id}' :: タスクが存在しないため、アノテーション {annotation_count} 件の削除をスキップします。")
|
|
285
|
+
failed_to_delete_annotation_count += annotation_count
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
if task["status"] == TaskStatus.WORKING.value:
|
|
289
|
+
logger.info(f"task_id='{task_id}' :: タスクが作業中状態のため、アノテーション {annotation_count} 件の削除をスキップします。")
|
|
290
|
+
failed_to_delete_annotation_count += annotation_count
|
|
291
|
+
continue
|
|
292
|
+
|
|
293
|
+
if not self.is_force: # noqa: SIM102
|
|
294
|
+
if task["status"] == TaskStatus.COMPLETE.value:
|
|
295
|
+
logger.info(
|
|
296
|
+
f"task_id='{task_id}' :: タスクが完了状態のため、アノテーション {annotation_count} 件の削除をスキップします。"
|
|
297
|
+
f"完了状態のタスクのアノテーションを削除するには、`--force`オプションを指定してください。"
|
|
298
|
+
)
|
|
299
|
+
failed_to_delete_annotation_count += annotation_count
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
if not self.confirm_processing(f"task_id='{task_id}'のタスクに含まれるアノテーション {annotation_count} 件を削除しますか?"):
|
|
303
|
+
failed_to_delete_annotation_count += annotation_count
|
|
304
|
+
continue
|
|
305
|
+
|
|
306
|
+
deleted_task_count += 1
|
|
307
|
+
for input_data_id, annotation_ids in sub_grouped.items():
|
|
308
|
+
# 指定input_data_idの全annotationを取得
|
|
309
|
+
# TODO どこかのタイミングで、"v=2"のアノテーションを取得するようにする
|
|
310
|
+
editor_annotation = self.service.wrapper.get_editor_annotation_or_none(
|
|
311
|
+
self.project_id, task_id=task_id, input_data_id=input_data_id, query_params={"v": "1"}
|
|
312
|
+
)
|
|
313
|
+
if editor_annotation is None:
|
|
314
|
+
logger.warning(
|
|
315
|
+
f"task_id='{task_id}'のタスクに、input_data_id='{input_data_id}'の入力データが含まれていません。 アノテーションの削除をスキップします。 :: " # noqa: E501
|
|
316
|
+
f"annotation_ids={annotation_ids}"
|
|
317
|
+
)
|
|
318
|
+
failed_to_delete_annotation_count += len(annotation_ids)
|
|
319
|
+
continue
|
|
320
|
+
|
|
321
|
+
if backup_dir is not None:
|
|
322
|
+
(backup_dir / task_id).mkdir(exist_ok=True, parents=True)
|
|
323
|
+
self.dump_annotation_obj.dump_editor_annotation(editor_annotation, json_path=backup_dir / task_id / f"{input_data_id}.json")
|
|
324
|
+
|
|
325
|
+
sub_deleted_annotation_count, sub_failed_to_delete_annotation_count = self.delete_annotation_by_annotation_ids(
|
|
326
|
+
editor_annotation, set(annotation_ids)
|
|
327
|
+
)
|
|
328
|
+
deleted_annotation_count += sub_deleted_annotation_count
|
|
329
|
+
failed_to_delete_annotation_count += sub_failed_to_delete_annotation_count
|
|
330
|
+
|
|
331
|
+
logger.info(
|
|
332
|
+
f"{deleted_task_count}/{task_count} 件のタスクに含まれている {deleted_annotation_count}/{total} 件のアノテーションを削除しました。"
|
|
333
|
+
f"{failed_to_delete_annotation_count} 件のアノテーションは削除できませんでした。"
|
|
334
|
+
)
|
|
335
|
+
|
|
164
336
|
|
|
165
337
|
class DeleteAnnotation(CommandLine):
|
|
166
338
|
"""
|
|
@@ -169,22 +341,9 @@ class DeleteAnnotation(CommandLine):
|
|
|
169
341
|
|
|
170
342
|
COMMON_MESSAGE = "annofabcli annotation delete: error:"
|
|
171
343
|
|
|
172
|
-
def main(self) -> None:
|
|
344
|
+
def main(self) -> None: # noqa: PLR0912
|
|
173
345
|
args = self.args
|
|
174
346
|
project_id = args.project_id
|
|
175
|
-
task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id)
|
|
176
|
-
|
|
177
|
-
if args.annotation_query is not None:
|
|
178
|
-
annotation_specs, _ = self.service.api.get_annotation_specs(project_id, query_params={"v": "2"})
|
|
179
|
-
try:
|
|
180
|
-
dict_annotation_query = get_json_from_args(args.annotation_query)
|
|
181
|
-
annotation_query_for_cli = AnnotationQueryForCLI.from_dict(dict_annotation_query)
|
|
182
|
-
annotation_query = annotation_query_for_cli.to_query_for_api(annotation_specs)
|
|
183
|
-
except ValueError as e:
|
|
184
|
-
print(f"{self.COMMON_MESSAGE} argument '--annotation_query' の値が不正です。{e}", file=sys.stderr) # noqa: T201
|
|
185
|
-
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
186
|
-
else:
|
|
187
|
-
annotation_query = None
|
|
188
347
|
|
|
189
348
|
if args.backup is None:
|
|
190
349
|
print( # noqa: T201
|
|
@@ -198,9 +357,55 @@ class DeleteAnnotation(CommandLine):
|
|
|
198
357
|
backup_dir = Path(args.backup)
|
|
199
358
|
|
|
200
359
|
super().validate_project(project_id, [ProjectMemberRole.OWNER])
|
|
201
|
-
|
|
202
360
|
main_obj = DeleteAnnotationMain(self.service, project_id, all_yes=args.yes, is_force=args.force)
|
|
203
|
-
|
|
361
|
+
|
|
362
|
+
if args.json is not None:
|
|
363
|
+
dict_annotation_list = get_json_from_args(args.json)
|
|
364
|
+
if not isinstance(dict_annotation_list, list):
|
|
365
|
+
print(f"{self.COMMON_MESSAGE} argument --json: JSON形式が不正です。オブジェクトの配列を指定してください。", file=sys.stderr) # noqa: T201
|
|
366
|
+
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
annotation_list = [DeletedAnnotationInfo(**eml) for eml in dict_annotation_list]
|
|
370
|
+
except TypeError as e:
|
|
371
|
+
print(f"{self.COMMON_MESSAGE} argument --json: 無効なオブジェクト形式です。{e}", file=sys.stderr) # noqa: T201
|
|
372
|
+
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
373
|
+
main_obj.delete_annotation_by_id_list(annotation_list, backup_dir=backup_dir)
|
|
374
|
+
|
|
375
|
+
elif args.csv is not None:
|
|
376
|
+
csv_path = Path(args.csv)
|
|
377
|
+
if not csv_path.exists():
|
|
378
|
+
print(f"{self.COMMON_MESSAGE} argument --csv: ファイルパスが存在しません。 '{args.csv}'", file=sys.stderr) # noqa: T201
|
|
379
|
+
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
380
|
+
|
|
381
|
+
df: pandas.DataFrame = pandas.read_csv(
|
|
382
|
+
args.csv,
|
|
383
|
+
dtype={"task_id": "string", "input_data_id": "string", "annotation_id": "string"},
|
|
384
|
+
)
|
|
385
|
+
required_cols = {"task_id", "input_data_id", "annotation_id"}
|
|
386
|
+
if not required_cols.issubset(df.columns):
|
|
387
|
+
print(f"{self.COMMON_MESSAGE} argument --csv: CSVに必須列がありません。{required_cols}", file=sys.stderr) # noqa: T201
|
|
388
|
+
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
389
|
+
|
|
390
|
+
annotation_list = [DeletedAnnotationInfo(**eml) for eml in df.to_dict(orient="records")]
|
|
391
|
+
main_obj.delete_annotation_by_id_list(annotation_list, backup_dir=backup_dir)
|
|
392
|
+
|
|
393
|
+
elif args.task_id is not None:
|
|
394
|
+
task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id)
|
|
395
|
+
|
|
396
|
+
if args.annotation_query is not None:
|
|
397
|
+
annotation_specs, _ = self.service.api.get_annotation_specs(project_id, query_params={"v": "2"})
|
|
398
|
+
try:
|
|
399
|
+
dict_annotation_query = get_json_from_args(args.annotation_query)
|
|
400
|
+
annotation_query_for_cli = AnnotationQueryForCLI.from_dict(dict_annotation_query)
|
|
401
|
+
annotation_query = annotation_query_for_cli.to_query_for_api(annotation_specs)
|
|
402
|
+
except ValueError as e:
|
|
403
|
+
print(f"{self.COMMON_MESSAGE} argument '--annotation_query' の値が不正です。{e}", file=sys.stderr) # noqa: T201
|
|
404
|
+
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
405
|
+
else:
|
|
406
|
+
annotation_query = None
|
|
407
|
+
|
|
408
|
+
main_obj.delete_annotation_for_task_list(task_id_list, annotation_query=annotation_query, backup_dir=backup_dir)
|
|
204
409
|
|
|
205
410
|
|
|
206
411
|
def main(args: argparse.Namespace) -> None:
|
|
@@ -213,21 +418,41 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
213
418
|
argument_parser = ArgumentParser(parser)
|
|
214
419
|
|
|
215
420
|
argument_parser.add_project_id()
|
|
216
|
-
argument_parser.add_task_id()
|
|
217
421
|
|
|
218
|
-
|
|
422
|
+
group = parser.add_mutually_exclusive_group(required=True)
|
|
423
|
+
group.add_argument(
|
|
424
|
+
"-t",
|
|
425
|
+
"--task_id",
|
|
426
|
+
type=str,
|
|
427
|
+
nargs="+",
|
|
428
|
+
help="削除対象のタスクのtask_idを指定します。 ``file://`` を先頭に付けると、task_idの一覧が記載されたファイルを指定できます。",
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
example_json = [{"task_id": "t1", "input_data_id": "i1", "annotation_id": "a1"}]
|
|
432
|
+
group.add_argument(
|
|
433
|
+
"--json",
|
|
434
|
+
type=str,
|
|
435
|
+
help=f"削除対象のアノテーションをJSON配列で指定します。例: ``{json.dumps(example_json)}``",
|
|
436
|
+
)
|
|
437
|
+
group.add_argument(
|
|
438
|
+
"--csv",
|
|
439
|
+
type=str,
|
|
440
|
+
help="削除対象のアノテーションを記載したCSVファイルを指定します。例: task_id,input_data_id,annotation_id",
|
|
441
|
+
)
|
|
219
442
|
|
|
443
|
+
EXAMPLE_ANNOTATION_QUERY = {"label": "car", "attributes": {"occluded": True}} # noqa: N806
|
|
220
444
|
parser.add_argument(
|
|
221
445
|
"-aq",
|
|
222
446
|
"--annotation_query",
|
|
223
447
|
type=str,
|
|
224
448
|
required=False,
|
|
225
449
|
help="削除対象のアノテーションを検索する条件をJSON形式で指定します。"
|
|
450
|
+
"``--csv`` または ``--json`` を指定した場合は、このオプションは無視されます。"
|
|
226
451
|
"``file://`` を先頭に付けると、JSON形式のファイルを指定できます。"
|
|
227
|
-
f"(ex): ``{EXAMPLE_ANNOTATION_QUERY}``",
|
|
452
|
+
f"(ex): ``{json.dumps(EXAMPLE_ANNOTATION_QUERY)}``",
|
|
228
453
|
)
|
|
229
454
|
|
|
230
|
-
parser.add_argument("--force", action="store_true", help="
|
|
455
|
+
parser.add_argument("--force", action="store_true", help="指定した場合は、完了状態のタスクのアノテーションも削除します。")
|
|
231
456
|
parser.add_argument(
|
|
232
457
|
"--backup",
|
|
233
458
|
type=str,
|
|
@@ -6,7 +6,7 @@ import json
|
|
|
6
6
|
import logging
|
|
7
7
|
import multiprocessing
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import Optional
|
|
9
|
+
from typing import Any, Optional
|
|
10
10
|
|
|
11
11
|
import annofabapi
|
|
12
12
|
from annofabapi.models import AnnotationDataHoldingType
|
|
@@ -24,17 +24,19 @@ class DumpAnnotationMain:
|
|
|
24
24
|
self.facade = AnnofabApiFacade(service)
|
|
25
25
|
self.project_id = project_id
|
|
26
26
|
|
|
27
|
-
def
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
def dump_editor_annotation(self, editor_annotation: dict[str, Any], json_path: Path) -> None:
|
|
28
|
+
"""
|
|
29
|
+
`getEditorAnnotation` APIのレスポンスをファイルに保存する。
|
|
30
|
+
"""
|
|
31
|
+
json_path.write_text(json.dumps(editor_annotation, ensure_ascii=False), encoding="utf-8")
|
|
31
32
|
|
|
32
|
-
details =
|
|
33
|
+
details = editor_annotation["details"]
|
|
33
34
|
outer_details = [e for e in details if e["data_holding_type"] == AnnotationDataHoldingType.OUTER.value]
|
|
34
35
|
if len(outer_details) == 0:
|
|
35
36
|
return
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
input_data_id = editor_annotation["input_data_id"]
|
|
39
|
+
outer_dir = json_path.parent / input_data_id
|
|
38
40
|
outer_dir.mkdir(exist_ok=True, parents=True)
|
|
39
41
|
|
|
40
42
|
# 塗りつぶし画像など外部リソースに保存されているファイルをダウンロードする
|
|
@@ -43,6 +45,11 @@ class DumpAnnotationMain:
|
|
|
43
45
|
outer_file_path = outer_dir / f"{annotation_id}"
|
|
44
46
|
self.service.wrapper.download(detail["url"], outer_file_path)
|
|
45
47
|
|
|
48
|
+
def dump_annotation_for_input_data(self, task_id: str, input_data_id: str, task_dir: Path) -> None:
|
|
49
|
+
editor_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id)
|
|
50
|
+
json_path = task_dir / f"{input_data_id}.json"
|
|
51
|
+
self.dump_editor_annotation(editor_annotation=editor_annotation, json_path=json_path)
|
|
52
|
+
|
|
46
53
|
def dump_annotation_for_task(self, task_id: str, output_dir: Path, *, task_index: Optional[int] = None) -> bool:
|
|
47
54
|
"""
|
|
48
55
|
タスク配下のアノテーションをファイルに保存する。
|