annofabcli 1.107.1__py3-none-any.whl → 1.109.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 +1 -1
- annofabcli/annotation/create_classification_annotation.py +408 -0
- annofabcli/annotation/delete_annotation.py +50 -14
- annofabcli/annotation/import_annotation.py +7 -8
- annofabcli/annotation/subcommand_annotation.py +2 -0
- annofabcli/input_data/subcommand_input_data.py +2 -2
- annofabcli/input_data/update_input_data.py +308 -0
- annofabcli/project/create_project.py +151 -0
- annofabcli/project/put_project.py +14 -129
- annofabcli/project/subcommand_project.py +6 -0
- annofabcli/project/update_configuration.py +151 -0
- annofabcli/project/update_project.py +298 -0
- annofabcli/statistics/list_video_duration.py +2 -2
- annofabcli/task/complete_tasks.py +20 -20
- {annofabcli-1.107.1.dist-info → annofabcli-1.109.0.dist-info}/METADATA +2 -2
- {annofabcli-1.107.1.dist-info → annofabcli-1.109.0.dist-info}/RECORD +19 -15
- annofabcli/input_data/change_input_data_name.py +0 -245
- {annofabcli-1.107.1.dist-info → annofabcli-1.109.0.dist-info}/WHEEL +0 -0
- {annofabcli-1.107.1.dist-info → annofabcli-1.109.0.dist-info}/entry_points.txt +0 -0
- {annofabcli-1.107.1.dist-info → annofabcli-1.109.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -130,7 +130,7 @@ class ChangeAnnotationAttributesMain(CommandLineWithConfirm):
|
|
|
130
130
|
tuple[0]: 成功した場合はTrue、失敗した場合はFalse
|
|
131
131
|
tuple[1]: 変更したアノテーションの個数
|
|
132
132
|
"""
|
|
133
|
-
logger_prefix = f"{task_index + 1!s}
|
|
133
|
+
logger_prefix = f"{task_index + 1!s} 件目 :: " if task_index is not None else ""
|
|
134
134
|
dict_task = self.service.wrapper.get_task_or_none(self.project_id, task_id)
|
|
135
135
|
if dict_task is None:
|
|
136
136
|
logger.warning(f"task_id = '{task_id}' は存在しません。")
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import multiprocessing
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
import annofabapi
|
|
10
|
+
from annofabapi.models import DefaultAnnotationType, ProjectMemberRole, TaskStatus
|
|
11
|
+
from annofabapi.util.annotation_specs import AnnotationSpecsAccessor
|
|
12
|
+
from annofabapi.utils import can_put_annotation
|
|
13
|
+
|
|
14
|
+
import annofabcli
|
|
15
|
+
from annofabcli.common.cli import (
|
|
16
|
+
COMMAND_LINE_ERROR_STATUS_CODE,
|
|
17
|
+
PARALLELISM_CHOICES,
|
|
18
|
+
ArgumentParser,
|
|
19
|
+
CommandLine,
|
|
20
|
+
CommandLineWithConfirm,
|
|
21
|
+
build_annofabapi_resource_and_login,
|
|
22
|
+
)
|
|
23
|
+
from annofabcli.common.facade import AnnofabApiFacade
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CreateClassificationAnnotationMain(CommandLineWithConfirm):
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
service: annofabapi.Resource,
|
|
32
|
+
*,
|
|
33
|
+
project_id: str,
|
|
34
|
+
all_yes: bool,
|
|
35
|
+
is_change_operator_to_me: bool,
|
|
36
|
+
include_completed: bool,
|
|
37
|
+
) -> None:
|
|
38
|
+
self.service = service
|
|
39
|
+
self.facade = AnnofabApiFacade(service)
|
|
40
|
+
CommandLineWithConfirm.__init__(self, all_yes)
|
|
41
|
+
|
|
42
|
+
self.project_id = project_id
|
|
43
|
+
self.is_change_operator_to_me = is_change_operator_to_me
|
|
44
|
+
self.include_completed = include_completed
|
|
45
|
+
|
|
46
|
+
# アノテーション仕様を取得
|
|
47
|
+
annotation_specs_v3, _ = self.service.api.get_annotation_specs(self.project_id, query_params={"v": "3"})
|
|
48
|
+
self.annotation_specs_accessor = AnnotationSpecsAccessor(annotation_specs_v3)
|
|
49
|
+
|
|
50
|
+
my_member, _ = self.service.api.get_my_member_in_project(project_id)
|
|
51
|
+
self.my_project_member_role = ProjectMemberRole(my_member["member_role"])
|
|
52
|
+
|
|
53
|
+
def _validate_and_prepare_task(self, task_id: str) -> tuple[Optional[dict[str, Any]], bool, Optional[str]]:
|
|
54
|
+
"""
|
|
55
|
+
タスクの検証と準備を行う
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
tuple[0]: タスク情報(Noneの場合は処理をスキップ)
|
|
59
|
+
tuple[1]: 担当者を変更したかどうか
|
|
60
|
+
tuple[2]: 元の担当者ID(担当者を変更した場合)
|
|
61
|
+
"""
|
|
62
|
+
# タスク情報を取得
|
|
63
|
+
task = self.service.wrapper.get_task_or_none(self.project_id, task_id)
|
|
64
|
+
if task is None:
|
|
65
|
+
logger.warning(f"task_id='{task_id}'であるタスクは存在しません。")
|
|
66
|
+
return None, False, None
|
|
67
|
+
|
|
68
|
+
if task["status"] == TaskStatus.WORKING.value:
|
|
69
|
+
logger.info(f"タスク'{task_id}'は作業中状態のため、全体アノテーションの作成をスキップします。")
|
|
70
|
+
return None, False, None
|
|
71
|
+
|
|
72
|
+
if not self.include_completed: # noqa: SIM102
|
|
73
|
+
if task["status"] == TaskStatus.COMPLETE.value:
|
|
74
|
+
logger.info(
|
|
75
|
+
f"タスク'{task_id}'は完了状態のため、全体アノテーションの作成をスキップします。完了状態のタスクに全体アノテーションを作成するには、 ``--include_completed`` を指定してください。"
|
|
76
|
+
)
|
|
77
|
+
return None, False, None
|
|
78
|
+
|
|
79
|
+
old_account_id: Optional[str] = None
|
|
80
|
+
changed_operator = False
|
|
81
|
+
|
|
82
|
+
if self.is_change_operator_to_me:
|
|
83
|
+
if not can_put_annotation(task, self.service.api.account_id, project_member_role=self.my_project_member_role):
|
|
84
|
+
logger.debug(f"タスク'{task_id}' の担当者を自分自身に変更します。")
|
|
85
|
+
old_account_id = task["account_id"]
|
|
86
|
+
task = self.service.wrapper.change_task_operator(
|
|
87
|
+
self.project_id,
|
|
88
|
+
task_id,
|
|
89
|
+
operator_account_id=self.service.api.account_id,
|
|
90
|
+
last_updated_datetime=task["updated_datetime"],
|
|
91
|
+
)
|
|
92
|
+
changed_operator = True
|
|
93
|
+
else: # noqa: PLR5501
|
|
94
|
+
if not can_put_annotation(task, self.service.api.account_id, project_member_role=self.my_project_member_role):
|
|
95
|
+
logger.debug(
|
|
96
|
+
f"タスク'{task_id}'は、過去に誰かに割り当てられたタスクで、現在の担当者が自分自身でないため、全体アノテーションの作成をスキップします。"
|
|
97
|
+
f"担当者を自分自身に変更して全体アノテーションを作成する場合は `--change_task_operator_to_me` を指定してください。"
|
|
98
|
+
)
|
|
99
|
+
return None, False, None
|
|
100
|
+
|
|
101
|
+
return task, changed_operator, old_account_id
|
|
102
|
+
|
|
103
|
+
def _create_annotation_details_for_labels(
|
|
104
|
+
self, task_id: str, input_data_id: str, labels: list[str], annotation_specs_accessor: AnnotationSpecsAccessor, existing_annotation_ids: set[str]
|
|
105
|
+
) -> list[dict[str, Any]]:
|
|
106
|
+
"""
|
|
107
|
+
ラベルリストから新しいアノテーション詳細を作成する
|
|
108
|
+
"""
|
|
109
|
+
new_details = []
|
|
110
|
+
for label_name in labels:
|
|
111
|
+
try:
|
|
112
|
+
label_info = annotation_specs_accessor.get_label(label_name=label_name)
|
|
113
|
+
except ValueError:
|
|
114
|
+
logger.warning(f"アノテーション仕様にラベル名(英語)が'{label_name}'であるラベル情報が存在しないか、または複数存在します。 :: task_id='{task_id}', input_data_id='{input_data_id}'")
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
# 全体アノテーション(Classification)かどうかチェック
|
|
118
|
+
if label_info["annotation_type"] != DefaultAnnotationType.CLASSIFICATION.value:
|
|
119
|
+
logger.warning(f"ラベル'{label_name}'は全体アノテーション(Classification)ではありません。 :: task_id='{task_id}', input_data_id='{input_data_id}'")
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
# 全体アノテーションのannotation_idはlabel_idと同じ
|
|
123
|
+
annotation_id = label_info["label_id"]
|
|
124
|
+
|
|
125
|
+
# すでに同じannotation_idのアノテーションが存在するかチェック
|
|
126
|
+
if annotation_id in existing_annotation_ids:
|
|
127
|
+
logger.debug(
|
|
128
|
+
f"task_id='{task_id}', input_data_id='{input_data_id}', label_name='{label_name}' :: "
|
|
129
|
+
f"既に全体アノテーションが存在するため、label_name='{label_name}'の全体アノテーションの作成をスキップします。"
|
|
130
|
+
)
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
# 新しいアノテーション詳細を作成
|
|
134
|
+
annotation_detail = {
|
|
135
|
+
"_type": "Create",
|
|
136
|
+
"label_id": label_info["label_id"],
|
|
137
|
+
"annotation_id": annotation_id,
|
|
138
|
+
"additional_data_list": [],
|
|
139
|
+
"editor_props": {},
|
|
140
|
+
"body": {"_type": "Inner", "data": {"_type": "Classification"}},
|
|
141
|
+
}
|
|
142
|
+
new_details.append(annotation_detail)
|
|
143
|
+
|
|
144
|
+
return new_details
|
|
145
|
+
|
|
146
|
+
def _put_annotations_for_input_data(self, task_id: str, input_data_id: str, new_details: list[dict[str, Any]], old_annotation: dict[str, Any]) -> int:
|
|
147
|
+
"""
|
|
148
|
+
入力データに対してアノテーションを登録する
|
|
149
|
+
"""
|
|
150
|
+
if len(new_details) == 0:
|
|
151
|
+
return 0
|
|
152
|
+
|
|
153
|
+
# 既存のアノテーションを更新モードに変更
|
|
154
|
+
for detail in old_annotation["details"]:
|
|
155
|
+
detail["_type"] = "Update"
|
|
156
|
+
detail["body"] = None
|
|
157
|
+
|
|
158
|
+
# リクエストボディを作成
|
|
159
|
+
request_body = {
|
|
160
|
+
"project_id": self.project_id,
|
|
161
|
+
"task_id": task_id,
|
|
162
|
+
"input_data_id": input_data_id,
|
|
163
|
+
"details": old_annotation["details"] + new_details,
|
|
164
|
+
"updated_datetime": old_annotation["updated_datetime"],
|
|
165
|
+
"format_version": "2.0.0",
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# アノテーションを登録
|
|
169
|
+
self.service.api.put_annotation(self.project_id, task_id, input_data_id, request_body=request_body, query_params={"v": "2"})
|
|
170
|
+
logger.debug(f"task_id='{task_id}', input_data_id='{input_data_id}' :: {len(new_details)} 件の全体アノテーションを作成しました。")
|
|
171
|
+
return len(new_details)
|
|
172
|
+
|
|
173
|
+
def create_classification_annotation_for_task(self, task_id: str, labels: list[str]) -> int:
|
|
174
|
+
"""
|
|
175
|
+
1個のタスクに対して全体アノテーションを作成します。
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
task_id: タスクID
|
|
179
|
+
labels: ラベル名のリスト
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
作成した全体アノテーションの個数
|
|
183
|
+
"""
|
|
184
|
+
# タスクの検証と準備
|
|
185
|
+
task, changed_operator, old_account_id = self._validate_and_prepare_task(task_id)
|
|
186
|
+
if task is None:
|
|
187
|
+
return 0
|
|
188
|
+
|
|
189
|
+
# タスクの入力データリストを取得
|
|
190
|
+
input_data_id_list = task["input_data_id_list"]
|
|
191
|
+
|
|
192
|
+
created_count = 0
|
|
193
|
+
try:
|
|
194
|
+
for input_data_id in input_data_id_list:
|
|
195
|
+
# 既存のアノテーションを取得
|
|
196
|
+
old_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id, query_params={"v": "2"})
|
|
197
|
+
|
|
198
|
+
# 既存のアノテーションIDを収集(重複チェック用)
|
|
199
|
+
existing_annotation_ids = {detail["annotation_id"] for detail in old_annotation["details"]}
|
|
200
|
+
|
|
201
|
+
# 新しいアノテーション詳細のリストを作成
|
|
202
|
+
new_details = self._create_annotation_details_for_labels(task_id, input_data_id, labels, self.annotation_specs_accessor, existing_annotation_ids)
|
|
203
|
+
|
|
204
|
+
# アノテーションを登録
|
|
205
|
+
created_count += self._put_annotations_for_input_data(task_id, input_data_id, new_details, old_annotation)
|
|
206
|
+
finally:
|
|
207
|
+
# 担当者を元に戻す
|
|
208
|
+
if changed_operator:
|
|
209
|
+
logger.debug(f"タスク'{task_id}' の担当者を元に戻します。")
|
|
210
|
+
self.service.wrapper.change_task_operator(
|
|
211
|
+
self.project_id,
|
|
212
|
+
task_id,
|
|
213
|
+
operator_account_id=old_account_id,
|
|
214
|
+
last_updated_datetime=task["updated_datetime"],
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return created_count
|
|
218
|
+
|
|
219
|
+
def execute_task(self, task_id: str, labels: list[str], task_index: Optional[int] = None) -> bool:
|
|
220
|
+
"""
|
|
221
|
+
1個のタスクに対して全体アノテーションを作成する。
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
task_id: タスクID
|
|
225
|
+
labels: ラベル名のリスト
|
|
226
|
+
task_index: タスクのインデックス
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
1個以上の全体アノテーションを作成したか
|
|
230
|
+
"""
|
|
231
|
+
if not self.confirm_processing(f"task_id='{task_id}' に全体アノテーション(label_name={labels})を作成しますか?"):
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
logger_prefix = f"{task_index + 1!s} 件目 :: " if task_index is not None else ""
|
|
235
|
+
logger.info(f"{logger_prefix}task_id='{task_id}' に対して処理します。")
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
created_count = self.create_classification_annotation_for_task(task_id, labels)
|
|
239
|
+
logger.info(f"{logger_prefix}task_id='{task_id}' :: {created_count} 件の全体アノテーションを作成しました。")
|
|
240
|
+
except Exception:
|
|
241
|
+
logger.warning(f"task_id='{task_id}' の全体アノテーション作成に失敗しました。", exc_info=True)
|
|
242
|
+
return False
|
|
243
|
+
else:
|
|
244
|
+
return created_count > 0
|
|
245
|
+
|
|
246
|
+
def execute_task_wrapper(
|
|
247
|
+
self,
|
|
248
|
+
tpl: tuple[int, str, list[str]],
|
|
249
|
+
) -> bool:
|
|
250
|
+
task_index, task_id, labels = tpl
|
|
251
|
+
try:
|
|
252
|
+
logger_prefix = f"{task_index + 1!s} 件目 :: "
|
|
253
|
+
logger.info(f"{logger_prefix}task_id='{task_id}' に対して処理します。")
|
|
254
|
+
|
|
255
|
+
created_count = self.create_classification_annotation_for_task(task_id, labels)
|
|
256
|
+
logger.info(f"{logger_prefix}task_id='{task_id}' :: {created_count} 件の全体アノテーションを作成しました。")
|
|
257
|
+
except Exception: # pylint: disable=broad-except
|
|
258
|
+
logger.warning(f"task_id='{task_id}' の全体アノテーション作成に失敗しました。", exc_info=True)
|
|
259
|
+
return False
|
|
260
|
+
else:
|
|
261
|
+
return created_count > 0
|
|
262
|
+
|
|
263
|
+
def main(self, task_ids: list[str], labels: list[str], parallelism: Optional[int] = None) -> None:
|
|
264
|
+
"""
|
|
265
|
+
メイン処理
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
task_ids: タスクIDのリスト
|
|
269
|
+
labels: ラベル名のリスト
|
|
270
|
+
parallelism: 並列度
|
|
271
|
+
"""
|
|
272
|
+
success_count = 0
|
|
273
|
+
|
|
274
|
+
if parallelism is not None:
|
|
275
|
+
with multiprocessing.Pool(parallelism) as pool:
|
|
276
|
+
task_args = [(task_index, task_id, labels) for task_index, task_id in enumerate(task_ids)]
|
|
277
|
+
result_bool_list = pool.map(self.execute_task_wrapper, task_args)
|
|
278
|
+
success_count = len([e for e in result_bool_list if e])
|
|
279
|
+
else:
|
|
280
|
+
for task_index, task_id in enumerate(task_ids):
|
|
281
|
+
result = self.execute_task(task_id, labels, task_index=task_index)
|
|
282
|
+
if result:
|
|
283
|
+
success_count += 1
|
|
284
|
+
|
|
285
|
+
logger.info(f"{success_count} / {len(task_ids)} 件のタスクに対して全体アノテーションを作成しました。")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class CreateClassificationAnnotation(CommandLine):
|
|
289
|
+
"""
|
|
290
|
+
全体アノテーション(Classification)を作成する
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
def __init__(self, service: annofabapi.Resource, facade: AnnofabApiFacade, args: argparse.Namespace) -> None:
|
|
294
|
+
super().__init__(service, facade, args)
|
|
295
|
+
self.args = args
|
|
296
|
+
|
|
297
|
+
@staticmethod
|
|
298
|
+
def validate(args: argparse.Namespace) -> bool:
|
|
299
|
+
COMMON_MESSAGE = "annofabcli annotation create_classification: error:" # noqa: N806
|
|
300
|
+
|
|
301
|
+
if not args.task_id:
|
|
302
|
+
print(f"{COMMON_MESSAGE} argument --task_id: タスクIDを指定してください。", file=sys.stderr) # noqa: T201
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
if not args.label_name:
|
|
306
|
+
print(f"{COMMON_MESSAGE} argument --label_name: ラベル名を指定してください。", file=sys.stderr) # noqa: T201
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
if args.parallelism is not None and not args.yes:
|
|
310
|
+
print( # noqa: T201
|
|
311
|
+
f"{COMMON_MESSAGE} argument --parallelism: '--parallelism'を指定するときは、'--yes' を指定してください。",
|
|
312
|
+
file=sys.stderr,
|
|
313
|
+
)
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
return True
|
|
317
|
+
|
|
318
|
+
def main(self) -> None:
|
|
319
|
+
args = self.args
|
|
320
|
+
if not self.validate(args):
|
|
321
|
+
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
322
|
+
|
|
323
|
+
project_id = args.project_id
|
|
324
|
+
|
|
325
|
+
super().validate_project(project_id, [ProjectMemberRole.OWNER, ProjectMemberRole.ACCEPTER])
|
|
326
|
+
|
|
327
|
+
if args.include_completed: # noqa: SIM102
|
|
328
|
+
if not self.facade.contains_any_project_member_role(project_id, [ProjectMemberRole.OWNER]):
|
|
329
|
+
print( # noqa: T201
|
|
330
|
+
"annofabcli annotation create_classification: error: argument --include_completed : "
|
|
331
|
+
"'--include_completed' 引数を利用するにはプロジェクトのオーナーロールを持つユーザーで実行する必要があります。",
|
|
332
|
+
file=sys.stderr,
|
|
333
|
+
)
|
|
334
|
+
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
335
|
+
|
|
336
|
+
task_ids = annofabcli.common.cli.get_list_from_args(args.task_id)
|
|
337
|
+
labels = annofabcli.common.cli.get_list_from_args(args.label_name)
|
|
338
|
+
|
|
339
|
+
main_obj = CreateClassificationAnnotationMain(
|
|
340
|
+
self.service,
|
|
341
|
+
project_id=project_id,
|
|
342
|
+
all_yes=self.all_yes,
|
|
343
|
+
is_change_operator_to_me=args.change_operator_to_me,
|
|
344
|
+
include_completed=args.include_completed,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
main_obj.main(task_ids, labels, parallelism=args.parallelism)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def main(args: argparse.Namespace) -> None:
|
|
351
|
+
service = build_annofabapi_resource_and_login(args)
|
|
352
|
+
facade = AnnofabApiFacade(service)
|
|
353
|
+
CreateClassificationAnnotation(service, facade, args).main()
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
357
|
+
argument_parser = ArgumentParser(parser)
|
|
358
|
+
|
|
359
|
+
argument_parser.add_project_id()
|
|
360
|
+
|
|
361
|
+
argument_parser.add_task_id(
|
|
362
|
+
required=True, help_message=("全体アノテーションの作成先であるタスクのtask_idを指定します。 ``file://`` を先頭に付けると、task_idの一覧が記載されたファイルを指定できます。")
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
parser.add_argument(
|
|
366
|
+
"--label_name",
|
|
367
|
+
type=str,
|
|
368
|
+
required=True,
|
|
369
|
+
nargs="+",
|
|
370
|
+
help="作成する全体アノテーションのラベル名(英語)を指定します。",
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
parser.add_argument(
|
|
374
|
+
"--change_operator_to_me",
|
|
375
|
+
action="store_true",
|
|
376
|
+
help="タスクの担当者を自分自身にしないとアノテーションを作成できない場合(過去に担当者が割り当てられていて現在の担当者が自分自身でない場合)、タスクの担当者を自分自身に変更してから全体アノテーションを作成します。アノテーションの作成が完了したら、タスクの担当者を元に戻します。",
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
parser.add_argument(
|
|
380
|
+
"--include_completed",
|
|
381
|
+
action="store_true",
|
|
382
|
+
help="完了状態のタスクにも全体アノテーションを作成します。ただし、オーナーロールを持つユーザーでしか実行できません。",
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
parser.add_argument(
|
|
386
|
+
"--parallelism",
|
|
387
|
+
type=int,
|
|
388
|
+
choices=PARALLELISM_CHOICES,
|
|
389
|
+
help="並列度。指定しない場合は、逐次的に処理します。``--parallelism`` を指定した場合は、``--yes`` も指定してください。",
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
parser.set_defaults(subcommand_func=main)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
|
|
396
|
+
subcommand_name = "create_classification"
|
|
397
|
+
subcommand_help = "全体アノテーション(Classification)を作成します。"
|
|
398
|
+
description = (
|
|
399
|
+
"指定したラベルの全体アノテーション(Classification)を作成します。"
|
|
400
|
+
"既に全体アノテーションが存在する場合はスキップします。"
|
|
401
|
+
"作業中状態のタスクには作成できません。"
|
|
402
|
+
"完了状態のタスクには、デフォルトでは作成できません。"
|
|
403
|
+
)
|
|
404
|
+
epilog = "オーナロールまたはチェッカーロールを持つユーザで実行してください。"
|
|
405
|
+
|
|
406
|
+
parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog)
|
|
407
|
+
parse_args(parser)
|
|
408
|
+
return parser
|
|
@@ -135,7 +135,7 @@ class DeleteAnnotationMain(CommandLineWithConfirm):
|
|
|
135
135
|
|
|
136
136
|
if not self.is_force: # noqa: SIM102
|
|
137
137
|
if task.status == TaskStatus.COMPLETE:
|
|
138
|
-
logger.info(f"task_id='{task_id}' :: タスクが完了状態のため、スキップします。完了状態のタスクのアノテーションを削除するには、`--
|
|
138
|
+
logger.info(f"task_id='{task_id}' :: タスクが完了状態のため、スキップします。完了状態のタスクのアノテーションを削除するには、`--include_completed`オプションを指定してください。")
|
|
139
139
|
return
|
|
140
140
|
|
|
141
141
|
annotation_list = self.get_annotation_list_for_task(task_id, annotation_query=annotation_query)
|
|
@@ -168,13 +168,49 @@ class DeleteAnnotationMain(CommandLineWithConfirm):
|
|
|
168
168
|
if backup_dir is not None:
|
|
169
169
|
backup_dir.mkdir(exist_ok=True, parents=True)
|
|
170
170
|
|
|
171
|
+
deleted_task_count = 0
|
|
172
|
+
failed_task_count = 0
|
|
171
173
|
for task_index, task_id in enumerate(task_id_list):
|
|
172
174
|
logger.info(f"{task_index + 1} / {len(task_id_list)} 件目: タスク '{task_id}' を削除します。")
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
|
|
176
|
+
# 削除前にタスクの状態を確認
|
|
177
|
+
dict_task = self.service.wrapper.get_task_or_none(self.project_id, task_id)
|
|
178
|
+
if dict_task is None:
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
task: Task = Task.from_dict(dict_task)
|
|
182
|
+
if task.status == TaskStatus.WORKING:
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
if not self.is_force and task.status == TaskStatus.COMPLETE:
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
# アノテーション一覧を取得して、削除対象があるかチェック
|
|
189
|
+
annotation_list = self.get_annotation_list_for_task(task_id, annotation_query=annotation_query)
|
|
190
|
+
if len(annotation_list) == 0:
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
# 確認処理でキャンセルされた場合はスキップ
|
|
194
|
+
if not self.confirm_processing(f"task_id='{task_id}'のタスクに含まれるアノテーション{len(annotation_list)}件を削除しますか?"):
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
# 実際に削除処理を実行
|
|
198
|
+
if backup_dir is not None:
|
|
199
|
+
self.dump_annotation_obj.dump_annotation_for_task(task_id, output_dir=backup_dir)
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
self.delete_annotation_list(annotation_list=annotation_list)
|
|
203
|
+
logger.info(f"task_id='{task_id}' :: アノテーション{len(annotation_list)}件を削除しました。")
|
|
204
|
+
deleted_task_count += 1
|
|
205
|
+
except requests.HTTPError:
|
|
206
|
+
logger.warning(f"task_id='{task_id}' :: アノテーション{len(annotation_list)}件の削除に失敗しました。一部のアノテーションは削除に成功している可能性があります。", exc_info=True)
|
|
207
|
+
failed_task_count += 1
|
|
208
|
+
|
|
209
|
+
# アノテーション削除処理の結果をログ出力
|
|
210
|
+
logger.info(
|
|
211
|
+
f"プロジェクト'{project_title}'に対して、{deleted_task_count}/{len(task_id_list)} 件のタスクのアノテーションを削除しました。 :: "
|
|
212
|
+
f"{failed_task_count}/{len(task_id_list)} 件のタスクはアノテーションの削除に失敗しました。"
|
|
213
|
+
)
|
|
178
214
|
|
|
179
215
|
def delete_annotation_by_annotation_ids(
|
|
180
216
|
self,
|
|
@@ -284,7 +320,7 @@ class DeleteAnnotationMain(CommandLineWithConfirm):
|
|
|
284
320
|
if task["status"] == TaskStatus.COMPLETE.value:
|
|
285
321
|
logger.info(
|
|
286
322
|
f"task_id='{task_id}' :: タスクが完了状態のため、アノテーション {annotation_count} 件の削除をスキップします。"
|
|
287
|
-
f"完了状態のタスクのアノテーションを削除するには、`--
|
|
323
|
+
f"完了状態のタスクのアノテーションを削除するには、`--include_completed`オプションを指定してください。"
|
|
288
324
|
)
|
|
289
325
|
failed_to_delete_annotation_count += annotation_count
|
|
290
326
|
continue
|
|
@@ -340,14 +376,14 @@ class DeleteAnnotation(CommandLine):
|
|
|
340
376
|
else:
|
|
341
377
|
backup_dir = Path(args.backup)
|
|
342
378
|
|
|
343
|
-
if args.
|
|
344
|
-
# --
|
|
345
|
-
# 完了状態のタスクを削除するには、オーナーロールである必要があるため、`args.
|
|
379
|
+
if args.include_completed:
|
|
380
|
+
# --include_completedオプションが指定されている場合は、完了状態のタスクも削除する
|
|
381
|
+
# 完了状態のタスクを削除するには、オーナーロールである必要があるため、`args.include_completed`で条件を分岐する
|
|
346
382
|
super().validate_project(project_id, [ProjectMemberRole.OWNER])
|
|
347
383
|
else:
|
|
348
384
|
super().validate_project(project_id, [ProjectMemberRole.OWNER, ProjectMemberRole.ACCEPTER])
|
|
349
385
|
|
|
350
|
-
main_obj = DeleteAnnotationMain(self.service, project_id, all_yes=args.yes, is_force=args.
|
|
386
|
+
main_obj = DeleteAnnotationMain(self.service, project_id, all_yes=args.yes, is_force=args.include_completed)
|
|
351
387
|
|
|
352
388
|
if args.json is not None:
|
|
353
389
|
dict_annotation_list = get_json_from_args(args.json)
|
|
@@ -415,7 +451,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
415
451
|
"--task_id",
|
|
416
452
|
type=str,
|
|
417
453
|
nargs="+",
|
|
418
|
-
help="
|
|
454
|
+
help="削除対象のアノテーションが含まれているタスクのtask_idを指定します。 ``file://`` を先頭に付けると、task_idの一覧が記載されたファイルを指定できます。",
|
|
419
455
|
)
|
|
420
456
|
|
|
421
457
|
example_json = [{"task_id": "t1", "input_data_id": "i1", "annotation_id": "a1"}]
|
|
@@ -443,7 +479,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
443
479
|
)
|
|
444
480
|
|
|
445
481
|
parser.add_argument(
|
|
446
|
-
"--
|
|
482
|
+
"--include_completed",
|
|
447
483
|
action="store_true",
|
|
448
484
|
help="指定した場合は、完了状態のタスクのアノテーションも削除します。ただし、完了状態のタスクを削除するには、オーナーロールを持つユーザーが実行する必要があります。",
|
|
449
485
|
)
|
|
@@ -463,7 +499,7 @@ def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argpa
|
|
|
463
499
|
"タスク配下のアノテーションを削除します。ただし、作業中状態のタスクのアノテーションは削除できません。"
|
|
464
500
|
"間違えてアノテーションを削除したときに復元できるようにするため、 ``--backup`` でバックアップ用のディレクトリを指定することを推奨します。"
|
|
465
501
|
)
|
|
466
|
-
epilog = "オーナーまたはチェッカーロールを持つユーザで実行してください。ただし``--
|
|
502
|
+
epilog = "オーナーまたはチェッカーロールを持つユーザで実行してください。ただし``--include_completed``オプションを指定した場合は、オーナーロールを持つユーザで実行してください。"
|
|
467
503
|
|
|
468
504
|
parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog)
|
|
469
505
|
parse_args(parser)
|
|
@@ -367,11 +367,15 @@ class AnnotationConverter:
|
|
|
367
367
|
|
|
368
368
|
request_detail = self.convert_annotation_detail(parser, detail, log_message_suffix=log_message_suffix)
|
|
369
369
|
except Exception as e:
|
|
370
|
-
logger.warning(
|
|
371
|
-
f"アノテーション情報を`putAnnotation`APIのリクエストボディへ変換するのに失敗しました。 :: {e!r} :: {log_message_suffix}",
|
|
372
|
-
)
|
|
373
370
|
if self.is_strict:
|
|
371
|
+
logger.warning(
|
|
372
|
+
f"アノテーション情報の一部を`putAnnotation`APIのリクエストボディへ変換できませんでした。 :: {e!r} :: {log_message_suffix}",
|
|
373
|
+
)
|
|
374
374
|
raise
|
|
375
|
+
|
|
376
|
+
logger.warning(
|
|
377
|
+
f"アノテーション情報の一部を`putAnnotation`APIのリクエストボディへ変換できませんでした。変換できたアノテーション情報のみ登録します。 :: {e!r} :: {log_message_suffix}",
|
|
378
|
+
)
|
|
375
379
|
continue
|
|
376
380
|
|
|
377
381
|
if detail.annotation_id in old_dict_detail:
|
|
@@ -434,11 +438,6 @@ class ImportAnnotationMain(CommandLineWithConfirm):
|
|
|
434
438
|
logger.debug(f"task_id='{task_id}', input_data_id='{input_data_id}' :: インポート元にアノテーションデータがないため、アノテーションの登録をスキップします。")
|
|
435
439
|
return 0
|
|
436
440
|
|
|
437
|
-
input_data = self.service.wrapper.get_input_data_or_none(self.project_id, input_data_id)
|
|
438
|
-
if input_data is None:
|
|
439
|
-
logger.warning(f"input_data_id='{input_data_id}'という入力データは存在しません。 :: task_id='{task_id}'")
|
|
440
|
-
return 0
|
|
441
|
-
|
|
442
441
|
old_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id, query_params={"v": "2"})
|
|
443
442
|
if len(old_annotation["details"]) > 0: # noqa: SIM102
|
|
444
443
|
if not self.is_overwrite and not self.is_merge:
|
|
@@ -5,6 +5,7 @@ import annofabcli.annotation.change_annotation_attributes
|
|
|
5
5
|
import annofabcli.annotation.change_annotation_attributes_per_annotation
|
|
6
6
|
import annofabcli.annotation.change_annotation_properties
|
|
7
7
|
import annofabcli.annotation.copy_annotation
|
|
8
|
+
import annofabcli.annotation.create_classification_annotation
|
|
8
9
|
import annofabcli.annotation.delete_annotation
|
|
9
10
|
import annofabcli.annotation.download_annotation_zip
|
|
10
11
|
import annofabcli.annotation.dump_annotation
|
|
@@ -25,6 +26,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
25
26
|
annofabcli.annotation.change_annotation_attributes_per_annotation.add_parser(subparsers)
|
|
26
27
|
annofabcli.annotation.change_annotation_properties.add_parser(subparsers)
|
|
27
28
|
annofabcli.annotation.copy_annotation.add_parser(subparsers)
|
|
29
|
+
annofabcli.annotation.create_classification_annotation.add_parser(subparsers)
|
|
28
30
|
annofabcli.annotation.delete_annotation.add_parser(subparsers)
|
|
29
31
|
annofabcli.annotation.download_annotation_zip.add_parser(subparsers)
|
|
30
32
|
annofabcli.annotation.dump_annotation.add_parser(subparsers)
|
|
@@ -3,7 +3,6 @@ from typing import Optional
|
|
|
3
3
|
|
|
4
4
|
import annofabcli
|
|
5
5
|
import annofabcli.common.cli
|
|
6
|
-
import annofabcli.input_data.change_input_data_name
|
|
7
6
|
import annofabcli.input_data.copy_input_data
|
|
8
7
|
import annofabcli.input_data.delete_input_data
|
|
9
8
|
import annofabcli.input_data.delete_metadata_key_of_input_data
|
|
@@ -13,6 +12,7 @@ import annofabcli.input_data.list_all_input_data_merged_task
|
|
|
13
12
|
import annofabcli.input_data.list_input_data
|
|
14
13
|
import annofabcli.input_data.put_input_data
|
|
15
14
|
import annofabcli.input_data.put_input_data_with_zip
|
|
15
|
+
import annofabcli.input_data.update_input_data
|
|
16
16
|
import annofabcli.input_data.update_metadata_of_input_data
|
|
17
17
|
|
|
18
18
|
|
|
@@ -20,7 +20,6 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
20
20
|
subparsers = parser.add_subparsers(dest="subcommand_name")
|
|
21
21
|
|
|
22
22
|
# サブコマンドの定義
|
|
23
|
-
annofabcli.input_data.change_input_data_name.add_parser(subparsers)
|
|
24
23
|
annofabcli.input_data.copy_input_data.add_parser(subparsers)
|
|
25
24
|
annofabcli.input_data.delete_input_data.add_parser(subparsers)
|
|
26
25
|
annofabcli.input_data.delete_metadata_key_of_input_data.add_parser(subparsers)
|
|
@@ -30,6 +29,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
30
29
|
annofabcli.input_data.list_all_input_data_merged_task.add_parser(subparsers)
|
|
31
30
|
annofabcli.input_data.put_input_data.add_parser(subparsers)
|
|
32
31
|
annofabcli.input_data.put_input_data_with_zip.add_parser(subparsers)
|
|
32
|
+
annofabcli.input_data.update_input_data.add_parser(subparsers)
|
|
33
33
|
annofabcli.input_data.update_metadata_of_input_data.add_parser(subparsers)
|
|
34
34
|
|
|
35
35
|
|