annofabcli 1.107.1__py3-none-any.whl → 1.108.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.
@@ -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} 件目: " if task_index is not None else ""
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}' :: タスクが完了状態のため、スキップします。完了状態のタスクのアノテーションを削除するには、`--force`オプションを指定してください。")
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
- self.delete_annotation_for_task(
174
- task_id,
175
- annotation_query=annotation_query,
176
- backup_dir=backup_dir,
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"完了状態のタスクのアノテーションを削除するには、`--force`オプションを指定してください。"
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.force:
344
- # --forceオプションが指定されている場合は、完了状態のタスクも削除する
345
- # 完了状態のタスクを削除するには、オーナーロールである必要があるため、`args.force`で条件を分岐する
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.force)
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="削除対象のタスクのtask_idを指定します。 ``file://`` を先頭に付けると、task_idの一覧が記載されたファイルを指定できます。",
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
- "--force",
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 = "オーナーまたはチェッカーロールを持つユーザで実行してください。ただし``--force``オプションを指定した場合は、オーナーロールを持つユーザで実行してください。"
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