Repository: kurusugawa-computer/annofab-cli (language: python)

Issue:
[annotation import, restore]並列化



Discussion / hints:

Fix patch:
diff --git a/annofabcli/annotation/change_annotation_attributes.py b/annofabcli/annotation/change_annotation_attributes.py
index d5580c6f..6b208469 100644
--- a/annofabcli/annotation/change_annotation_attributes.py
+++ b/annofabcli/annotation/change_annotation_attributes.py
@@ -1,22 +1,26 @@
+from __future__ import annotations
+
 import argparse
+import dataclasses
+import functools
 import logging
+import multiprocessing
 import sys
-from collections import defaultdict
 from enum import Enum
 from pathlib import Path
 from typing import Any, Dict, List, Optional
 
 import annofabapi
-import requests
 from annofabapi.dataclass.task import Task
 from annofabapi.models import ProjectMemberRole, TaskStatus
 
 import annofabcli
 from annofabcli import AnnofabApiFacade
-from annofabcli.annotation.dump_annotation import DumpAnnotation
+from annofabcli.annotation.dump_annotation import DumpAnnotationMain
 from annofabcli.common.cli import (
     COMMAND_LINE_ERROR_STATUS_CODE,
     AbstractCommandLineInterface,
+    AbstractCommandLineWithConfirmInterface,
     ArgumentParser,
     build_annofabapi_resource_and_login,
     get_json_from_args,
@@ -31,27 +35,66 @@ class ChangeBy(Enum):
     INPUT_DATA = "input_data"
 
 
-class ChangeAttributesOfAnnotation(AbstractCommandLineInterface):
-    """
-    アノテーション属性を変更
-    """
+class ChangeAnnotationAttributesMain(AbstractCommandLineWithConfirmInterface):
+    def __init__(
+        self,
+        service: annofabapi.Resource,
+        *,
+        project_id: str,
+        is_force: bool,
+        all_yes: bool,
+    ):
+        self.service = service
+        self.facade = AnnofabApiFacade(service)
+        AbstractCommandLineWithConfirmInterface.__init__(self, all_yes)
 
-    COMMON_MESSAGE = "annofabcli annotation change_attributes: error:"
+        self.project_id = project_id
+        self.is_force = is_force
+
+        self.dump_annotation_obj = DumpAnnotationMain(service, project_id)
+
+    def change_annotation_attributes(
+        self, annotation_list: List[Dict[str, Any]], attributes: List[AdditionalData]
+    ) -> Optional[List[Dict[str, Any]]]:
+        """
+        アノテーション属性値を変更する。
+
+        Args:
+            annotation_list: 変更対象のアノテーション一覧
+            attributes: 変更後の属性値
+
+        Returns:
+            `batch_update_annotations`メソッドのレスポンス
+
+        """
 
-    def __init__(self, service: annofabapi.Resource, facade: AnnofabApiFacade, args: argparse.Namespace):
-        super().__init__(service, facade, args)
-        self.dump_annotation_obj = DumpAnnotation(service, facade, args)
+        def _to_request_body_elm(annotation: Dict[str, Any]) -> Dict[str, Any]:
+            detail = annotation["detail"]
+            return {
+                "data": {
+                    "project_id": annotation["project_id"],
+                    "task_id": annotation["task_id"],
+                    "input_data_id": annotation["input_data_id"],
+                    "updated_datetime": annotation["updated_datetime"],
+                    "annotation_id": detail["annotation_id"],
+                    "label_id": detail["label_id"],
+                    "additional_data_list": attributes_for_dict,
+                },
+                "_type": "Put",
+            }
+
+        attributes_for_dict: List[Dict[str, Any]] = [dataclasses.asdict(e) for e in attributes]
+        request_body = [_to_request_body_elm(annotation) for annotation in annotation_list]
+        return self.service.api.batch_update_annotations(self.project_id, request_body)[0]
 
     def change_attributes_for_task(
         self,
-        project_id: str,
         task_id: str,
         annotation_query: AnnotationQuery,
         attributes: List[AdditionalData],
-        change_by: ChangeBy,
-        force: bool = False,
         backup_dir: Optional[Path] = None,
-    ) -> None:
+        task_index: Optional[int] = None,
+    ) -> bool:
         """
         タスクに対してアノテーション属性を変更する。
 
@@ -63,90 +106,133 @@ def change_attributes_for_task(
             force: タスクのステータスによらず更新する
             backup_dir: アノテーションをバックアップとして保存するディレクトリ。指定しない場合は、バックアップを取得しない。
 
+        Returns:
+            アノテーションの属性を変更するAPI ``change_annotation_attributes`` を実行したか否か
         """
-        dict_task = self.service.wrapper.get_task_or_none(project_id, task_id)
+        logger_prefix = f"{str(task_index+1)} 件目: " if task_index is not None else ""
+        dict_task = self.service.wrapper.get_task_or_none(self.project_id, task_id)
         if dict_task is None:
             logger.warning(f"task_id = '{task_id}' は存在しません。")
-            return
+            return False
 
         task: Task = Task.from_dict(dict_task)
-        logger.info(
-            f"task_id={task.task_id}, phase={task.phase.value}, status={task.status.value}, "
-            f"updated_datetime={task.updated_datetime}"
-        )
         if task.status == TaskStatus.WORKING:
             logger.warning(f"task_id={task_id}: タスクが作業中状態のため、スキップします。")
-            return
+            return False
 
-        if not force:
+        if not self.is_force:
             if task.status == TaskStatus.COMPLETE:
                 logger.warning(f"task_id={task_id}: タスクが完了状態のため、スキップします。")
-                return
+                return False
 
-        annotation_list = self.facade.get_annotation_list_for_task(project_id, task_id, query=annotation_query)
-        logger.info(f"task_id='{task_id}'の変更対象アノテーション数：{len(annotation_list)}")
+        annotation_list = self.facade.get_annotation_list_for_task(self.project_id, task_id, query=annotation_query)
+        logger.info(
+            f"{logger_prefix}task_id='{task_id}'の変更対象アノテーション数：{len(annotation_list)}, phase={task.phase.value}, status={task.status.value}, updated_datetime={task.updated_datetime}"  # noqa: E501
+        )
         if len(annotation_list) == 0:
-            logger.info(f"task_id='{task_id}'には変更対象のアノテーションが存在しないので、スキップします。")
-            return
+            logger.info(f"{logger_prefix}task_id='{task_id}'には変更対象のアノテーションが存在しないので、スキップします。")
+            return False
 
         if not self.confirm_processing(f"task_id='{task_id}' のアノテーション属性を変更しますか？"):
-            return
+            return False
 
         if backup_dir is not None:
-            self.dump_annotation_obj.dump_annotation_for_task(project_id, task_id, output_dir=backup_dir)
+            self.dump_annotation_obj.dump_annotation_for_task(task_id, output_dir=backup_dir)
 
-        try:
-            if change_by == ChangeBy.INPUT_DATA:
-                dict_annotation_list_by_input_data = defaultdict(list)
-                for annotation in annotation_list:
-                    dict_annotation_list_by_input_data[annotation["input_data_id"]].append(annotation)
-
-                for input_data_id, annotation_list_by_input_data in dict_annotation_list_by_input_data.items():
-                    logger.debug(
-                        f"task_id={task_id}, input_data_id={input_data_id}: "
-                        f"{len(annotation_list_by_input_data)}個のアノテーションの属性を変更しました。"
-                    )
-                    self.facade.change_annotation_attributes(project_id, annotation_list_by_input_data, attributes)
-            else:
-                self.facade.change_annotation_attributes(project_id, annotation_list, attributes)
+        self.change_annotation_attributes(annotation_list, attributes)
+        logger.info(f"{logger_prefix}task_id={task_id}: アノテーション属性を変更しました。")
+        return True
 
-            logger.info(f"task_id={task_id}: アノテーション属性を変更しました。")
-        except requests.HTTPError as e:
-            logger.warning(e)
-            logger.warning(f"task_id={task_id}: アノテーション属性の変更に失敗しました。")
+    def change_attributes_for_task_wrapper(
+        self,
+        tpl: tuple[int, str],
+        annotation_query: AnnotationQuery,
+        attributes: List[AdditionalData],
+        backup_dir: Optional[Path] = None,
+    ) -> bool:
+        task_index, task_id = tpl
+        try:
+            return self.change_attributes_for_task(
+                task_id,
+                annotation_query=annotation_query,
+                attributes=attributes,
+                backup_dir=backup_dir,
+                task_index=task_index,
+            )
+        except Exception:  # pylint: disable=broad-except
+            logger.warning(f"タスク'{task_id}'のアノテーションの属性の変更に失敗しました。", exc_info=True)
+            return False
 
-    def change_annotation_attributes(
+    def change_annotation_attributes_for_task_list(
         self,
-        project_id: str,
         task_id_list: List[str],
         annotation_query: AnnotationQuery,
         attributes: List[AdditionalData],
-        change_by: ChangeBy = ChangeBy.TASK,
-        force: bool = False,
         backup_dir: Optional[Path] = None,
+        parallelism: Optional[int] = None,
     ):
-        super().validate_project(project_id, [ProjectMemberRole.OWNER])
-
-        project_title = self.facade.get_project_title(project_id)
+        project_title = self.facade.get_project_title(self.project_id)
         logger.info(f"プロジェクト'{project_title}'に対して、タスク{len(task_id_list)} 件のアノテーションの属性を変更します。")
 
         if backup_dir is not None:
             backup_dir.mkdir(exist_ok=True, parents=True)
 
-        for task_index, task_id in enumerate(task_id_list):
-            logger.info(f"{task_index+1} / {len(task_id_list)} 件目: タスク '{task_id}' のアノテーションの属性を変更します。")
-            self.change_attributes_for_task(
-                project_id,
-                task_id,
+        success_count = 0
+        if parallelism is not None:
+            func = functools.partial(
+                self.change_attributes_for_task_wrapper,
                 annotation_query=annotation_query,
                 attributes=attributes,
-                force=force,
-                change_by=change_by,
                 backup_dir=backup_dir,
             )
+            with multiprocessing.Pool(parallelism) as pool:
+                result_bool_list = pool.map(func, enumerate(task_id_list))
+                success_count = len([e for e in result_bool_list if e])
+
+        else:
+
+            for task_index, task_id in enumerate(task_id_list):
+
+                try:
+                    result = self.change_attributes_for_task(
+                        task_id,
+                        annotation_query=annotation_query,
+                        attributes=attributes,
+                        backup_dir=backup_dir,
+                        task_index=task_index,
+                    )
+                    if result:
+                        success_count += 1
+                except Exception:
+                    logger.warning(f"タスク'{task_id}'のアノテーションの属性の変更に失敗しました。", exc_info=True)
+                    continue
+
+        logger.info(f"{success_count} / {len(task_id_list)} 件のタスクに対してアノテーションの属性を変更しました。")
+
+
+class ChangeAttributesOfAnnotation(AbstractCommandLineInterface):
+    """
+    アノテーション属性を変更
+    """
+
+    COMMON_MESSAGE = "annofabcli annotation change_attributes: error:"
+
+    def validate(self, args: argparse.Namespace) -> bool:
+        if args.parallelism is not None and not args.yes:
+            print(
+                f"{self.COMMON_MESSAGE} argument --parallelism: '--parallelism'を指定するときは、'--yes' を指定してください。",
+                file=sys.stderr,
+            )
+            return False
+
+        return True
 
     def main(self):
         args = self.args
+
+        if not self.validate(args):
+            sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
+
         project_id = args.project_id
         task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id)
 
@@ -174,14 +260,17 @@ def main(self):
         else:
             backup_dir = Path(args.backup)
 
-        self.change_annotation_attributes(
-            project_id,
+        super().validate_project(project_id, [ProjectMemberRole.OWNER])
+
+        main_obj = ChangeAnnotationAttributesMain(
+            self.service, project_id=project_id, is_force=args.force, all_yes=args.yes
+        )
+        main_obj.change_annotation_attributes_for_task_list(
             task_id_list,
             annotation_query=annotation_query,
             attributes=attributes,
-            force=args.force,
-            change_by=ChangeBy(args.change_by),
             backup_dir=backup_dir,
+            parallelism=args.parallelism,
         )
 
 
@@ -221,20 +310,18 @@ def parse_args(parser: argparse.ArgumentParser):
 
     parser.add_argument("--force", action="store_true", help="完了状態のタスクのアノテーション属性も変更します。")
 
-    parser.add_argument(
-        "--change_by",
-        type=str,
-        choices=[ChangeBy.TASK.value, ChangeBy.INPUT_DATA.value],
-        default=ChangeBy.TASK.value,
-        help="アノテーション属性の変更単位を指定してください。[Deprecated] 廃止される可能性があります。",
-    )
-
     parser.add_argument(
         "--backup",
         type=str,
         required=False,
         help="アノテーションのバックアップを保存するディレクトリを指定してください。アノテーションの復元は ``annotation restore`` コマンドで実現できます。",
     )
+    parser.add_argument(
+        "--parallelism",
+        type=int,
+        help="並列度。指定しない場合は、逐次的に処理します。指定した場合は、``--yes`` も指定してください。",
+    )
+
     parser.set_defaults(subcommand_func=main)
 
 
diff --git a/annofabcli/annotation/change_annotation_properties.py b/annofabcli/annotation/change_annotation_properties.py
index 5a1db184..9cbf46a7 100644
--- a/annofabcli/annotation/change_annotation_properties.py
+++ b/annofabcli/annotation/change_annotation_properties.py
@@ -1,5 +1,9 @@
+from __future__ import annotations
+
 import argparse
+import functools
 import logging
+import multiprocessing
 import sys
 from dataclasses import dataclass
 from pathlib import Path
@@ -12,10 +16,11 @@
 
 import annofabcli
 from annofabcli import AnnofabApiFacade
-from annofabcli.annotation.dump_annotation import DumpAnnotation
+from annofabcli.annotation.dump_annotation import DumpAnnotationMain
 from annofabcli.common.cli import (
     COMMAND_LINE_ERROR_STATUS_CODE,
     AbstractCommandLineInterface,
+    AbstractCommandLineWithConfirmInterface,
     ArgumentParser,
     build_annofabapi_resource_and_login,
     get_json_from_args,
@@ -30,111 +35,110 @@ class AnnotationDetailForCli(DataClassJsonMixin):
     is_protected: Optional[bool] = None
 
 
-class ChangePropertiesOfAnnotation(AbstractCommandLineInterface):
-    """
-    アノテーションのプロパティを変更
-    """
+class ChangePropertiesOfAnnotationMain(AbstractCommandLineWithConfirmInterface):
+    def __init__(
+        self,
+        service: annofabapi.Resource,
+        project_id: str,
+        *,
+        is_force: bool,
+        all_yes: bool,
+    ):
+        self.service = service
+        self.facade = AnnofabApiFacade(service)
+        self.is_force = is_force
+        AbstractCommandLineWithConfirmInterface.__init__(self, all_yes)
+        self.project_id = project_id
+        self.dump_annotation_obj = DumpAnnotationMain(service, project_id)
 
-    COMMON_MESSAGE = "annofabcli annotation change_properties: error:"
+    def change_annotation_properties(
+        self,
+        task_id: str,
+        annotation_list: List[SingleAnnotation],
+        properties: AnnotationDetailForCli,
+    ):
+        """
+        アノテーションのプロパティを変更する。
+
+        Args:
+            annotation_list: 変更対象のアノテーション一覧
+            properties: 変更後のプロパティ
 
-    def __init__(self, service: annofabapi.Resource, facade: AnnofabApiFacade, args: argparse.Namespace):
-        super().__init__(service, facade, args)
-        self.dump_annotation_obj = DumpAnnotation(service, facade, args)
+        """
+
+        now_datetime = str_now()
+
+        def to_properties_from_cli(
+            annotation_list: List[SingleAnnotation], properties: AnnotationDetailForCli
+        ) -> List[Dict[str, Any]]:
+            annotations_for_api = []
+            annotation_details_by_input_data: Dict[str, List[Dict[str, Any]]] = {}
+
+            # input_data_idごとにannotation_listを分ける
+            for annotation in annotation_list:
+                input_data_id = annotation["input_data_id"]
+                if input_data_id not in annotation_details_by_input_data:
+                    annotation_details_by_input_data[input_data_id] = []
+                annotation_details_by_input_data[input_data_id].append(annotation["detail"])
+
+            for input_data_id, annotation_details in annotation_details_by_input_data.items():
+                annotations, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id)
+                for detail in annotations["details"]:
+                    # "url"が存在するとエラーになるため、取り除く
+                    detail.pop("url", None)
+                    for annotation_detail in annotation_details:
+                        if annotation_detail["annotation_id"] != detail["annotation_id"]:
+                            continue
+                        detail["is_protected"] = properties.is_protected
+                        # 更新日時も一緒に変更する
+                        detail["updated_datetime"] = now_datetime
+                annotations_for_api.append(annotations)
+            return annotations_for_api
+
+        def _to_request_body_elm(annotation: Dict[str, Any]):
+            return self.service.api.put_annotation(
+                annotation["project_id"],
+                annotation["task_id"],
+                annotation["input_data_id"],
+                {
+                    "project_id": annotation["project_id"],
+                    "task_id": annotation["task_id"],
+                    "input_data_id": annotation["input_data_id"],
+                    "details": annotation["details"],
+                    "updated_datetime": annotation["updated_datetime"],
+                },
+            )
+
+        annotations = to_properties_from_cli(annotation_list, properties)
+        for annotation in annotations:
+            _to_request_body_elm(annotation)
 
     def change_properties_for_task(  # pylint: disable=too-many-return-statements
         self,
-        project_id: str,
         task_id: str,
         properties: AnnotationDetailForCli,
         annotation_query: Optional[AnnotationQuery] = None,
-        force: bool = False,
         backup_dir: Optional[Path] = None,
+        task_index: Optional[int] = None,
     ) -> bool:
         """
         タスクに対してアノテーションのプロパティを変更する。
 
         Args:
-            project_id:
             task_id:
             annotation_query: 変更対象のアノテーションの検索条件
             properties: 変更後のアノテーションのプロパティ
-            force: タスクのステータスによらず更新する
             backup_dir: アノテーションをバックアップとして保存するディレクトリ。指定しない場合は、バックアップを取得しない。
 
         """
-
-        def change_annotation_properties(
-            self,
-            project_id: str,
-            task_id: str,
-            annotation_list: List[SingleAnnotation],
-            properties: AnnotationDetailForCli,
-        ):
-            """
-            アノテーションのプロパティを変更する。
-
-            【注意】取扱注意
-
-            Args:
-                annotation_list: 変更対象のアノテーション一覧
-                properties: 変更後のプロパティ
-
-            Returns:
-                ``メソッドのレスポンス
-
-            """
-
-            now_datetime = str_now()
-
-            def to_properties_from_cli(
-                annotation_list: List[SingleAnnotation], properties: AnnotationDetailForCli
-            ) -> List[Dict[str, Any]]:
-                annotations_for_api = []
-                annotation_details_by_input_data: Dict[str, List[Dict[str, Any]]] = {}
-
-                for annotation in annotation_list:
-                    input_data_id = annotation["input_data_id"]
-                    if input_data_id not in annotation_details_by_input_data:
-                        annotation_details_by_input_data[input_data_id] = []
-                    annotation_details_by_input_data[input_data_id].append(annotation["detail"])
-
-                for input_data_id, annotation_details in annotation_details_by_input_data.items():
-                    annotations, _ = self.service.api.get_editor_annotation(project_id, task_id, input_data_id)
-                    for single_annotation in annotations["details"]:
-                        for annotation_detail in annotation_details:
-                            if annotation_detail["annotation_id"] != single_annotation["annotation_id"]:
-                                continue
-                            single_annotation["is_protected"] = properties.is_protected
-                            # 更新日時も一緒に変更する
-                            single_annotation["updated_datetime"] = now_datetime
-                    annotations_for_api.append(annotations)
-                return annotations_for_api
-
-            def _to_request_body_elm(annotation: Dict[str, Any]):
-                return self.service.api.put_annotation(
-                    annotation["project_id"],
-                    annotation["task_id"],
-                    annotation["input_data_id"],
-                    {
-                        "project_id": annotation["project_id"],
-                        "task_id": annotation["task_id"],
-                        "input_data_id": annotation["input_data_id"],
-                        "details": annotation["details"],
-                        "updated_datetime": annotation["updated_datetime"],
-                    },
-                )
-
-            annotations = to_properties_from_cli(annotation_list, properties)
-            for annotation in annotations:
-                _to_request_body_elm(annotation)
-
-        dict_task = self.service.wrapper.get_task_or_none(project_id, task_id)
+        logger_prefix = f"{str(task_index+1)} 件目: " if task_index is not None else ""
+        dict_task = self.service.wrapper.get_task_or_none(self.project_id, task_id)
         if dict_task is None:
             logger.warning(f"task_id = '{task_id}' は存在しません。")
             return False
 
         logger.debug(
-            f"task_id={task_id}, phase={dict_task['phase']}, status={dict_task['status']}, "
+            f"{logger_prefix}task_id={task_id}, phase={dict_task['phase']}, status={dict_task['status']}, "
             f"updated_datetime={dict_task['updated_datetime']}"
         )
 
@@ -142,17 +146,19 @@ def _to_request_body_elm(annotation: Dict[str, Any]):
             logger.info(f"タスク'{task_id}'は作業中または受入完了状態のため、アノテーションプロパティの変更をスキップします。 status={dict_task['status']}")
             return False
 
-        old_account_id: Optional[str] = None
+        old_account_id: Optional[str] = dict_task["account_id"]
         changed_operator = False
 
-        if force:
+        if self.is_force:
             if not can_put_annotation(dict_task, self.service.api.account_id):
                 logger.debug(f"タスク'{task_id}' の担当者を自分自身に変更します。")
-                self.service.wrapper.change_task_operator(
-                    project_id, task_id, operator_account_id=self.service.api.account_id
+                dict_task = self.service.wrapper.change_task_operator(
+                    self.project_id,
+                    task_id,
+                    operator_account_id=self.service.api.account_id,
+                    last_updated_datetime=dict_task["updated_datetime"],
                 )
                 changed_operator = True
-                old_account_id = dict_task["account_id"]
 
         else:
             if not can_put_annotation(dict_task, self.service.api.account_id):
@@ -162,8 +168,8 @@ def _to_request_body_elm(annotation: Dict[str, Any]):
                 )
                 return False
 
-        annotation_list = self.facade.get_annotation_list_for_task(project_id, task_id, query=annotation_query)
-        logger.info(f"task_id='{task_id}'の変更対象アノテーション数：{len(annotation_list)}")
+        annotation_list = self.facade.get_annotation_list_for_task(self.project_id, task_id, query=annotation_query)
+        logger.info(f"{logger_prefix}task_id='{task_id}'の変更対象アノテーション数：{len(annotation_list)}")
         if len(annotation_list) == 0:
             logger.info(f"task_id='{task_id}'には変更対象のアノテーションが存在しないので、スキップします。")
             return False
@@ -172,11 +178,11 @@ def _to_request_body_elm(annotation: Dict[str, Any]):
             return False
 
         if backup_dir is not None:
-            self.dump_annotation_obj.dump_annotation_for_task(project_id, task_id, output_dir=backup_dir)
+            self.dump_annotation_obj.dump_annotation_for_task(task_id, output_dir=backup_dir)
 
         try:
-            change_annotation_properties(self, project_id, task_id, annotation_list, properties)
-            logger.info(f"task_id={task_id}: アノテーションのプロパティを変更しました。")
+            self.change_annotation_properties(task_id, annotation_list, properties)
+            logger.info(f"{logger_prefix}task_id={task_id}: アノテーションのプロパティを変更しました。")
             return True
         except Exception:  # pylint: disable=broad-except
             logger.warning(f"task_id={task_id}: アノテーションのプロパティの変更に失敗しました。", exc_info=True)
@@ -184,43 +190,103 @@ def _to_request_body_elm(annotation: Dict[str, Any]):
         finally:
             if changed_operator:
                 logger.debug(f"タスク'{task_id}' の担当者を元に戻します。")
-                old_account_id = dict_task["account_id"]
-                self.service.wrapper.change_task_operator(project_id, task_id, operator_account_id=old_account_id)
+                self.service.wrapper.change_task_operator(
+                    self.project_id,
+                    task_id,
+                    operator_account_id=old_account_id,
+                    last_updated_datetime=dict_task["updated_datetime"],
+                )
 
-    def change_annotation_properties(
+    def change_properties_for_task_wrapper(
+        self,
+        tpl: tuple[int, str],
+        properties: AnnotationDetailForCli,
+        annotation_query: Optional[AnnotationQuery] = None,
+        backup_dir: Optional[Path] = None,
+    ) -> bool:
+        task_index, task_id = tpl
+        try:
+            return self.change_properties_for_task(
+                task_id,
+                properties=properties,
+                annotation_query=annotation_query,
+                backup_dir=backup_dir,
+                task_index=task_index,
+            )
+        except Exception:  # pylint: disable=broad-except
+            logger.warning(f"task_id={task_id}: アノテーションのプロパティの変更に失敗しました。", exc_info=True)
+            return False
+
+    def change_annotation_properties_task_list(
         self,
-        project_id: str,
         task_id_list: List[str],
         properties: AnnotationDetailForCli,
         annotation_query: Optional[AnnotationQuery] = None,
-        force: bool = False,
         backup_dir: Optional[Path] = None,
+        parallelism: Optional[int] = None,
     ):
-        super().validate_project(project_id, [ProjectMemberRole.OWNER])
 
-        project_title = self.facade.get_project_title(project_id)
-        logger.info(f"プロジェクト'{project_title}'に対して、タスク{len(task_id_list)} 件のアノテーションのプロパティを変更します。")
+        project_title = self.facade.get_project_title(self.project_id)
+        logger.info(f"プロジェクト'{project_title}'に対して、タスク{len(task_id_list)} 件のアノテーションのプロパティを変更します")
 
         if backup_dir is not None:
             backup_dir.mkdir(exist_ok=True, parents=True)
 
-        for task_index, task_id in enumerate(task_id_list):
-            logger.debug(f"{task_index+1} / {len(task_id_list)} 件目: タスク '{task_id}' のアノテーションのプロパティを変更します。")
+        success_count = 0
+        if parallelism is not None:
+            func = functools.partial(
+                self.change_properties_for_task_wrapper,
+                properties=properties,
+                annotation_query=annotation_query,
+                backup_dir=backup_dir,
+            )
+            with multiprocessing.Pool(parallelism) as pool:
+                result_bool_list = pool.map(func, enumerate(task_id_list))
+                success_count = len([e for e in result_bool_list if e])
 
-            try:
-                self.change_properties_for_task(
-                    project_id,
-                    task_id,
-                    properties=properties,
-                    annotation_query=annotation_query,
-                    force=force,
-                    backup_dir=backup_dir,
-                )
-            except Exception:  # pylint: disable=broad-except
-                logger.warning(f"task_id={task_id}: アノテーションのプロパティの変更に失敗しました。", exc_info=True)
+        else:
+            for task_index, task_id in enumerate(task_id_list):
+                logger.debug(f"{task_index+1} / {len(task_id_list)} 件目: タスク '{task_id}' のアノテーションのプロパティを変更します。")
+
+                try:
+                    result = self.change_properties_for_task(
+                        task_id,
+                        properties=properties,
+                        annotation_query=annotation_query,
+                        backup_dir=backup_dir,
+                    )
+                    if result:
+                        success_count += 1
+                except Exception:  # pylint: disable=broad-except
+                    logger.warning(f"task_id={task_id}: アノテーションのプロパティの変更に失敗しました。", exc_info=True)
+                    continue
+
+        logger.info(f"{success_count} / {len(task_id_list)} 件のタスクに対してアノテーションのプロパティを変更しました。")
+
+
+class ChangePropertiesOfAnnotation(AbstractCommandLineInterface):
+    """
+    アノテーションのプロパティを変更
+    """
+
+    COMMON_MESSAGE = "annofabcli annotation change_properties: error:"
+
+    def validate(self, args: argparse.Namespace) -> bool:
+        if args.parallelism is not None and not args.yes:
+            print(
+                f"{self.COMMON_MESSAGE} argument --parallelism: '--parallelism'を指定するときは、'--yes' を指定してください。",
+                file=sys.stderr,
+            )
+            return False
+
+        return True
 
     def main(self):
         args = self.args
+
+        if not self.validate(args):
+            sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
+
         project_id = args.project_id
         task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id)
 
@@ -246,13 +312,17 @@ def main(self):
         else:
             backup_dir = Path(args.backup)
 
-        self.change_annotation_properties(
-            project_id,
+        super().validate_project(project_id, [ProjectMemberRole.OWNER])
+
+        main_obj = ChangePropertiesOfAnnotationMain(
+            self.service, project_id=project_id, is_force=args.force, all_yes=args.yes
+        )
+        main_obj.change_annotation_properties_task_list(
             task_id_list,
             properties=properties_for_cli,
             annotation_query=annotation_query,
-            force=args.force,
             backup_dir=backup_dir,
+            parallelism=args.parallelism,
         )
 
 
@@ -303,6 +373,13 @@ def parse_args(parser: argparse.ArgumentParser):
         required=False,
         help="アノテーションのバックアップを保存するディレクトリを指定してください。アノテーションの復元は ``annotation restore`` コマンドで実現できます。",
     )
+
+    parser.add_argument(
+        "--parallelism",
+        type=int,
+        help="並列度。指定しない場合は、逐次的に処理します。指定した場合は、``--yes`` も指定してください。",
+    )
+
     parser.set_defaults(subcommand_func=main)
 
 
diff --git a/annofabcli/annotation/copy_annotation.py b/annofabcli/annotation/copy_annotation.py
index 63d20f69..1fc83d05 100644
--- a/annofabcli/annotation/copy_annotation.py
+++ b/annofabcli/annotation/copy_annotation.py
@@ -3,6 +3,7 @@
 import argparse
 import copy
 import logging
+import multiprocessing
 import sys
 from abc import ABC, abstractmethod
 from dataclasses import dataclass
@@ -122,27 +123,35 @@ def get_copy_target_list(str_copy_target_list: list[str]) -> list[CopyTarget]:
 
 
 class CopyAnnotationMain(AbstractCommandLineWithConfirmInterface):
-    def __init__(self, service: annofabapi.Resource, *, all_yes: bool, overwrite: bool, merge: bool, force: bool):
+    def __init__(
+        self, service: annofabapi.Resource, *, project_id: str, all_yes: bool, overwrite: bool, merge: bool, force: bool
+    ):
 
         self.service = service
+        self.project_id = project_id
         self.overwrite = overwrite
         self.merge = merge
         self.force = force
 
         AbstractCommandLineWithConfirmInterface.__init__(self, all_yes)
 
-    def copy_annotation_by_task(self, project_id: str, copy_target: CopyTargetByTask):
-        """タスク単位でアノテーションをコピーする"""
-        src_task = self.service.wrapper.get_task_or_none(project_id=project_id, task_id=copy_target.src_task_id)
-        dest_task = self.service.wrapper.get_task_or_none(project_id=project_id, task_id=copy_target.dest_task_id)
+    def copy_annotation_by_task(self, copy_target: CopyTargetByTask) -> bool:
+        """
+        タスク単位でアノテーションをコピーする
+
+        Returns:
+            1フレーム以上のアノテーションをコピーしたどうか
+        """
+        src_task = self.service.wrapper.get_task_or_none(project_id=self.project_id, task_id=copy_target.src_task_id)
+        dest_task = self.service.wrapper.get_task_or_none(project_id=self.project_id, task_id=copy_target.dest_task_id)
 
         if src_task is None:
             logger.warning(f"コピー元のタスク '{copy_target.src_task_id}' は存在しません。")
-            return
+            return False
 
         if dest_task is None:
             logger.warning(f"コピー先のタスク '{copy_target.dest_task_id}' は存在しません。")
-            return
+            return False
 
         src_input_data_id_list = src_task["input_data_id_list"]
         dest_input_data_id_list = dest_task["input_data_id_list"]
@@ -159,7 +168,6 @@ def copy_annotation_by_task(self, project_id: str, copy_target: CopyTargetByTask
         for src_input_data_id, dest_input_data_id in zip(src_input_data_id_list, dest_input_data_id_list):
             try:
                 result = self.copy_annotation_by_input_data(
-                    project_id,
                     CopyTargetByInputData(
                         src_task_id=copy_target.src_task_id,
                         dest_task_id=copy_target.dest_task_id,
@@ -173,6 +181,7 @@ def copy_annotation_by_task(self, project_id: str, copy_target: CopyTargetByTask
                 logger.warning(f"'{copy_target.src}'のアノテーションを'{copy_target.dest}'にコピーするのに失敗しました。", exc_info=True)
 
         logger.debug(f"'{copy_target.src_task_id}'の{copy_count}フレームのアノテーションを、'{copy_target.dest_task_id}'にコピーしました。")
+        return copy_count > 0
 
     @staticmethod
     def _merge_annotation(
@@ -193,16 +202,34 @@ def _merge_annotation(
                 details.append(src_anno)
         return details
 
-    def copy_annotation_by_input_data(self, project_id: str, copy_target: CopyTargetByInputData) -> bool:
-        """入力データ単位でアノテーションをコピーする"""
-        src_annotation, _ = self.service.api.get_editor_annotation(
-            project_id=project_id, task_id=copy_target.src_task_id, input_data_id=copy_target.src_input_data_id
+    def copy_annotation_by_input_data(self, copy_target: CopyTargetByInputData) -> bool:
+        """
+        入力データ単位でアノテーションをコピーする。
+
+        Returns:
+            アノテーションをコピーしたかどうか。
+
+        """
+        src_annotation = self.service.wrapper.get_editor_annotation_or_none(
+            project_id=self.project_id, task_id=copy_target.src_task_id, input_data_id=copy_target.src_input_data_id
         )
+        if src_annotation is None:
+            logger.warning(
+                f"task_id='{copy_target.src_task_id}'のタスクが存在しないか、またはtask_id='{copy_target.src_task_id}'のタスクにinput_data_id='{copy_target.src_input_data_id}'の入力データが存在しません。"  # noqa: E501
+            )
+            return False
+
         src_anno_details = src_annotation["details"]
 
-        dest_annotation, _ = self.service.api.get_editor_annotation(
-            project_id=project_id, task_id=copy_target.dest_task_id, input_data_id=copy_target.dest_input_data_id
+        dest_annotation = self.service.wrapper.get_editor_annotation_or_none(
+            project_id=self.project_id, task_id=copy_target.dest_task_id, input_data_id=copy_target.dest_input_data_id
         )
+        if dest_annotation is None:
+            logger.warning(
+                f"task_id='{copy_target.dest_task_id}'のタスクが存在しないか、またはtask_id='{copy_target.dest_task_id}'のタスクにinput_data_id='{copy_target.dest_input_data_id}'の入力データが存在しません。"  # noqa: E501
+            )
+            return False
+
         dest_anno_details = dest_annotation["details"]
 
         anno_details = []
@@ -219,7 +246,7 @@ def copy_annotation_by_input_data(self, project_id: str, copy_target: CopyTarget
             return False
 
         request_body = self.service.wrapper._create_request_body_for_copy_annotation(
-            project_id,
+            self.project_id,
             copy_target.dest_task_id,
             copy_target.dest_input_data_id,
             src_details=anno_details,
@@ -227,57 +254,107 @@ def copy_annotation_by_input_data(self, project_id: str, copy_target: CopyTarget
         )
         request_body["updated_datetime"] = dest_annotation["updated_datetime"]
         self.service.api.put_annotation(
-            project_id, copy_target.dest_task_id, copy_target.dest_input_data_id, request_body=request_body
+            self.project_id, copy_target.dest_task_id, copy_target.dest_input_data_id, request_body=request_body
         )
         logger.debug(f"'{copy_target.src}'のアノテーションを'{copy_target.dest}'にコピーしました。")
         return True
 
-    def copy_annotations(self, project_id: str, copy_target_list: list[CopyTarget]):
-        for copy_target in copy_target_list:
-
-            if not self.confirm_processing(f"'{copy_target.src}'のアノテーションを、'{copy_target.dest}'にコピーしますか？"):
-                continue
-
-            dest_task = self.service.wrapper.get_task_or_none(project_id, copy_target.dest_task_id)
-            if dest_task is None:
-                logger.warning(f"コピー先のタスク '{copy_target.dest_task_id}' は存在しません。")
-                continue
-
-            # 担当者割り当て変更チェック
-            changed_operator = False
-            original_operator = dest_task["account_id"]
-            if not can_put_annotation(dest_task, self.service.api.account_id):
-                if self.force:
-                    logger.debug(f"`--force` が指定されているため，コピー先タスク'{copy_target.dest_task_id}' の担当者を自分自身に変更します。")
-                    changed_operator = True
-                    self.service.wrapper.change_task_operator(
-                        project_id, copy_target.dest_task_id, self.service.api.account_id
-                    )
-                else:
-                    logger.debug(
-                        f"コピー先タスク'{copy_target.dest_task_id}'は、過去に誰かに割り当てられたタスクで、"
-                        f"現在の担当者が自分自身でないため、アノテーションのコピーをスキップします。"
-                        f"担当者を自分自身に変更してアノテーションをコピーする場合は `--force` を指定してください。"
-                    )
-                    continue
+    def copy_annotation(self, copy_target: CopyTarget) -> bool:
+        dest_task = self.service.wrapper.get_task_or_none(self.project_id, copy_target.dest_task_id)
+        if dest_task is None:
+            logger.warning(f"コピー先のタスク '{copy_target.dest_task_id}' は存在しません。")
+            return False
+
+        if not self.confirm_processing(f"'{copy_target.src}'のアノテーションを、'{copy_target.dest}'にコピーしますか？"):
+            return False
+
+        # 担当者割り当て変更チェック
+        changed_operator = False
+        original_operator = dest_task["account_id"]
+        if not can_put_annotation(dest_task, self.service.api.account_id):
+            if self.force:
+                logger.debug(f"`--force` が指定されているため，コピー先タスク'{copy_target.dest_task_id}' の担当者を自分自身に変更します。")
+                changed_operator = True
+                dest_task = self.service.wrapper.change_task_operator(
+                    self.project_id,
+                    copy_target.dest_task_id,
+                    self.service.api.account_id,
+                    last_updated_datetime=dest_task["updated_datetime"],
+                )
+            else:
+                logger.debug(
+                    f"コピー先タスク'{copy_target.dest_task_id}'は、過去に誰かに割り当てられたタスクで、"
+                    f"現在の担当者が自分自身でないため、アノテーションのコピーをスキップします。"
+                    f"担当者を自分自身に変更してアノテーションをコピーする場合は `--force` を指定してください。"
+                )
+                return False
+
+        result = False
+        if isinstance(copy_target, CopyTargetByTask):
+            result = self.copy_annotation_by_task(copy_target)
+
+        elif isinstance(copy_target, CopyTargetByInputData):
+            result = self.copy_annotation_by_input_data(copy_target)
+
+        # 担当者を元に戻す
+        if changed_operator:
+            self.service.wrapper.change_task_operator(
+                self.project_id,
+                copy_target.dest_task_id,
+                original_operator,
+                last_updated_datetime=dest_task["updated_datetime"],
+            )
 
-            if isinstance(copy_target, CopyTargetByTask):
-                self.copy_annotation_by_task(project_id, copy_target)
+        return result
 
-            elif isinstance(copy_target, CopyTargetByInputData):
-                self.copy_annotation_by_input_data(project_id, copy_target)
+    def copy_annotation_wrapper(self, copy_target: CopyTarget) -> bool:
+        try:
+            return self.copy_annotation(copy_target)
+        except Exception:  # pylint: disable=broad-except
+            logger.warning(f"'{copy_target.src}'のアノテーションを'{copy_target.dest}'へコピーするのに失敗しました。", exc_info=True)
+            return False
 
-            # 担当者を元に戻す
-            if changed_operator:
-                self.service.wrapper.change_task_operator(project_id, copy_target.dest_task_id, original_operator)
-        return True
+    def copy_annotations(self, copy_target_list: list[CopyTarget], *, parallelism: Optional[int] = None):
+        if parallelism is not None:
+            with multiprocessing.Pool(parallelism) as pool:
+                result_bool_list = pool.map(self.copy_annotation_wrapper, copy_target_list)
+                success_count = len([e for e in result_bool_list if e])
+
+        else:
+            # 逐次処理
+            success_count = 0
+            for copy_target in copy_target_list:
+                try:
+                    result = self.copy_annotation(
+                        copy_target,
+                    )
+                    if result:
+                        success_count += 1
+                except Exception:  # pylint: disable=broad-except
+                    logger.warning(f"'{copy_target.src}'のアノテーションを'{copy_target.dest}'へコピーするのに失敗しました。", exc_info=True)
+                    continue
+
+        logger.info(f"{success_count} / {len(copy_target_list)} 件のタスクまたは入力データに対して、アノテーションをコピーしました。")
 
 
 class CopyAnnotation(AbstractCommandLineInterface):
     COMMON_MESSAGE = "annofabcli annotation copy: error:"
 
+    def validate(self, args: argparse.Namespace) -> bool:
+        if args.parallelism is not None and not args.yes:
+            print(
+                f"{self.COMMON_MESSAGE} argument --parallelism: '--parallelism'を指定するときは、'--yes' を指定してください。",
+                file=sys.stderr,
+            )
+            return False
+
+        return True
+
     def main(self):
         args = self.args
+        if not self.validate(args):
+            sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
+
         project_id = args.project_id
         str_copy_target_list = get_list_from_args(args.input)
 
@@ -287,9 +364,14 @@ def main(self):
             sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
 
         main_obj = CopyAnnotationMain(
-            self.service, all_yes=self.all_yes, overwrite=args.overwrite, merge=args.merge, force=args.force
+            self.service,
+            project_id=project_id,
+            all_yes=self.all_yes,
+            overwrite=args.overwrite,
+            merge=args.merge,
+            force=args.force,
         )
-        main_obj.copy_annotations(project_id, copy_target_list)
+        main_obj.copy_annotations(copy_target_list, parallelism=args.parallelism)
 
 
 def main(args):
@@ -326,13 +408,19 @@ def parse_args(parser: argparse.ArgumentParser):
         "--force", action="store_true", help="過去に割り当てられていて現在の担当者が自分自身でない場合、タスクの担当者を一時的に自分自身に変更してからアノテーションをコピーします。"
     )
 
+    parser.add_argument(
+        "--parallelism",
+        type=int,
+        help="並列度。指定しない場合は、逐次的に処理します。指定した場合は、``--yes`` も指定してください。",
+    )
+
     parser.set_defaults(subcommand_func=main)
 
 
 def add_parser(subparsers: Optional[argparse._SubParsersAction] = None):
     subcommand_name = "copy"
     subcommand_help = "アノテーションをコピーします．"
-    description = "アノテーションをコピーします。"
+    description = "タスク単位または入力データ単位で、アノテーションをコピーします。"
     parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description)
     parse_args(parser)
     return parser
diff --git a/annofabcli/annotation/delete_annotation.py b/annofabcli/annotation/delete_annotation.py
index 2da46667..a67e72c5 100644
--- a/annofabcli/annotation/delete_annotation.py
+++ b/annofabcli/annotation/delete_annotation.py
@@ -1,8 +1,10 @@
+from __future__ import annotations
+
 import argparse
 import logging
 import sys
 from pathlib import Path
-from typing import List, Optional
+from typing import Any, List, Optional
 
 import annofabapi
 import requests
@@ -11,10 +13,11 @@
 
 import annofabcli
 from annofabcli import AnnofabApiFacade
-from annofabcli.annotation.dump_annotation import DumpAnnotation
+from annofabcli.annotation.dump_annotation import DumpAnnotationMain
 from annofabcli.common.cli import (
     COMMAND_LINE_ERROR_STATUS_CODE,
     AbstractCommandLineInterface,
+    AbstractCommandLineWithConfirmInterface,
     ArgumentParser,
     build_annofabapi_resource_and_login,
     get_json_from_args,
@@ -24,35 +27,67 @@
 logger = logging.getLogger(__name__)
 
 
-class DeleteAnnotation(AbstractCommandLineInterface):
-    """
-    アノテーションを削除する
+class DeleteAnnotationMain(AbstractCommandLineWithConfirmInterface):
+    """アノテーション削除処理用のクラス
+
+    Args:
+        is_force: 完了状態のタスクを削除するかどうか
     """
 
-    def __init__(self, service: annofabapi.Resource, facade: AnnofabApiFacade, args: argparse.Namespace):
-        super().__init__(service, facade, args)
-        self.dump_annotation_obj = DumpAnnotation(service, facade, args)
+    def __init__(
+        self,
+        service: annofabapi.Resource,
+        project_id: str,
+        *,
+        is_force: bool,
+        all_yes: bool,
+    ):
+        self.service = service
+        self.facade = AnnofabApiFacade(service)
+        self.is_force = is_force
+        AbstractCommandLineWithConfirmInterface.__init__(self, all_yes)
+        self.project_id = project_id
+        self.dump_annotation_obj = DumpAnnotationMain(service, project_id)
+
+    def delete_annotation_list(self, annotation_list: list[dict[str, Any]]):
+        """
+        アノテーション一覧を削除する。
+
+        Args:
+            annotation_list: アノテーション一覧
+
+        """
+
+        def _to_request_body_elm(annotation: dict[str, Any]) -> dict[str, Any]:
+            detail = annotation["detail"]
+            return {
+                "project_id": annotation["project_id"],
+                "task_id": annotation["task_id"],
+                "input_data_id": annotation["input_data_id"],
+                "updated_datetime": annotation["updated_datetime"],
+                "annotation_id": detail["annotation_id"],
+                "_type": "Delete",
+            }
+
+        request_body = [_to_request_body_elm(annotation) for annotation in annotation_list]
+        self.service.api.batch_update_annotations(self.project_id, request_body)
 
     def delete_annotation_for_task(
         self,
-        project_id: str,
         task_id: str,
         annotation_query: Optional[AnnotationQuery] = None,
-        force: bool = False,
         backup_dir: Optional[Path] = None,
     ) -> None:
         """
         タスクに対してアノテーションを削除する
 
         Args:
-            project_id:
             task_id:
             annotation_query: 削除対象のアノテーションの検索条件
-            force: 完了状態のタスクのアノテーションを削除するかどうか
             backup_dir: 削除対象のアノテーションをバックアップとして保存するディレクトリ。指定しない場合は、バックアップを取得しない。
 
         """
-        dict_task = self.service.wrapper.get_task_or_none(project_id, task_id)
+        dict_task = self.service.wrapper.get_task_or_none(self.project_id, task_id)
         if dict_task is None:
             logger.warning(f"task_id = '{task_id}' は存在しません。")
             return
@@ -67,12 +102,12 @@ def delete_annotation_for_task(
             logger.warning(f"task_id={task_id}: タスクが作業中状態のため、スキップします。")
             return
 
-        if not force:
+        if not self.is_force:
             if task.status == TaskStatus.COMPLETE:
                 logger.warning(f"task_id={task_id}: タスクが完了状態のため、スキップします。")
                 return
 
-        annotation_list = self.facade.get_annotation_list_for_task(project_id, task_id, query=annotation_query)
+        annotation_list = self.facade.get_annotation_list_for_task(self.project_id, task_id, query=annotation_query)
         logger.info(f"task_id='{task_id}'の削除対象アノテーション数：{len(annotation_list)}")
         if len(annotation_list) == 0:
             logger.info(f"task_id='{task_id}'には削除対象のアノテーションが存在しないので、スキップします。")
@@ -82,26 +117,21 @@ def delete_annotation_for_task(
             return
 
         if backup_dir is not None:
-            self.dump_annotation_obj.dump_annotation_for_task(project_id, task_id, output_dir=backup_dir)
+            self.dump_annotation_obj.dump_annotation_for_task(task_id, output_dir=backup_dir)
 
         try:
-            self.facade.delete_annotation_list(project_id, annotation_list=annotation_list)
+            self.delete_annotation_list(annotation_list=annotation_list)
             logger.info(f"task_id={task_id}: アノテーションを削除しました。")
-        except requests.HTTPError as e:
-            logger.warning(e)
-            logger.warning(f"task_id={task_id}: アノテーションの削除に失敗しました。")
+        except requests.HTTPError:
+            logger.warning(f"task_id={task_id}: アノテーションの削除に失敗しました。", exc_info=True)
 
     def delete_annotation_for_task_list(
         self,
-        project_id: str,
         task_id_list: List[str],
         annotation_query: Optional[AnnotationQuery] = None,
-        force: bool = False,
         backup_dir: Optional[Path] = None,
     ):
-        super().validate_project(project_id, [ProjectMemberRole.OWNER])
-
-        project_title = self.facade.get_project_title(project_id)
+        project_title = self.facade.get_project_title(self.project_id)
         logger.info(f"プロジェクト'{project_title}'に対して、タスク{len(task_id_list)} 件のアノテーションを削除します。")
 
         if backup_dir is not None:
@@ -110,13 +140,17 @@ def delete_annotation_for_task_list(
         for task_index, task_id in enumerate(task_id_list):
             logger.info(f"{task_index+1} / {len(task_id_list)} 件目: タスク '{task_id}' を削除します。")
             self.delete_annotation_for_task(
-                project_id,
                 task_id,
                 annotation_query=annotation_query,
-                force=force,
                 backup_dir=backup_dir,
             )
 
+
+class DeleteAnnotation(AbstractCommandLineInterface):
+    """
+    アノテーションを削除する
+    """
+
     def main(self):
         args = self.args
         project_id = args.project_id
@@ -140,9 +174,10 @@ def main(self):
         else:
             backup_dir = Path(args.backup)
 
-        self.delete_annotation_for_task_list(
-            project_id, task_id_list, annotation_query=annotation_query, backup_dir=backup_dir, force=args.force
-        )
+        super().validate_project(project_id, [ProjectMemberRole.OWNER])
+
+        main_obj = DeleteAnnotationMain(self.service, project_id, all_yes=args.yes, is_force=args.force)
+        main_obj.delete_annotation_for_task_list(task_id_list, annotation_query=annotation_query, backup_dir=backup_dir)
 
 
 def main(args):
diff --git a/annofabcli/annotation/dump_annotation.py b/annofabcli/annotation/dump_annotation.py
index 5a9cb0d3..5d69dcfe 100644
--- a/annofabcli/annotation/dump_annotation.py
+++ b/annofabcli/annotation/dump_annotation.py
@@ -1,10 +1,14 @@
+from __future__ import annotations
+
 import argparse
+import functools
 import json
 import logging
+import multiprocessing
 from pathlib import Path
 from typing import List, Optional
 
-import requests
+import annofabapi
 from annofabapi.models import AnnotationDataHoldingType
 
 import annofabcli
@@ -14,13 +18,14 @@
 logger = logging.getLogger(__name__)
 
 
-class DumpAnnotation(AbstractCommandLineInterface):
-    """
-    アノテーション情報をダンプする
-    """
+class DumpAnnotationMain:
+    def __init__(self, service: annofabapi.Resource, project_id: str):
+        self.service = service
+        self.facade = AnnofabApiFacade(service)
+        self.project_id = project_id
 
-    def dump_annotation_for_input_data(self, project_id: str, task_id: str, input_data_id: str, task_dir: Path) -> None:
-        annotation, _ = self.service.api.get_editor_annotation(project_id, task_id, input_data_id)
+    def dump_annotation_for_input_data(self, task_id: str, input_data_id: str, task_dir: Path) -> None:
+        annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id)
         json_path = task_dir / f"{input_data_id}.json"
         json_path.write_text(json.dumps(annotation, ensure_ascii=False), encoding="utf-8")
 
@@ -32,36 +37,25 @@ def dump_annotation_for_input_data(self, project_id: str, task_id: str, input_da
         outer_dir = task_dir / input_data_id
         outer_dir.mkdir(exist_ok=True, parents=True)
 
+        # 塗りつぶし画像など外部リソースに保存されているファイルをダウンロードする
         for detail in outer_details:
-            if not detail["data_holding_type"] == AnnotationDataHoldingType.OUTER.value:
-                continue
-
-            outer_file_url = detail["url"]
-            response = self.service.api.session.get(outer_file_url)
-            if response.status_code != requests.codes.ok:
-                logger.warning(
-                    f"塗りつぶし画像ファイルのダウンロード失敗しました。"
-                    f"status_code={response.status_code}, url={response.url}, text={response.text}"
-                )
-                continue
-
             annotation_id = detail["annotation_id"]
             outer_file_path = outer_dir / f"{annotation_id}"
-            outer_file_path.write_bytes(response.content)
+            self.service.wrapper.download(detail["url"], outer_file_path)
 
-    def dump_annotation_for_task(self, project_id: str, task_id: str, output_dir: Path) -> bool:
+    def dump_annotation_for_task(self, task_id: str, output_dir: Path, *, task_index: Optional[int] = None) -> bool:
         """
         タスク配下のアノテーションをファイルに保存する。
 
         Args:
-            project_id:
             task_id:
             output_dir: 保存先。配下に"task_id"のディレクトリを作成する。
 
         Returns:
             アノテーション情報をファイルに保存したかどうか。
         """
-        task = self.service.wrapper.get_task_or_none(project_id, task_id)
+        logger_prefix = f"{str(task_index+1)} 件目: " if task_index is not None else ""
+        task = self.service.wrapper.get_task_or_none(self.project_id, task_id)
         if task is None:
             logger.warning(f"task_id = '{task_id}' のタスクは存在しません。スキップします。")
             return False
@@ -69,31 +63,68 @@ def dump_annotation_for_task(self, project_id: str, task_id: str, output_dir: Pa
         input_data_id_list = task["input_data_id_list"]
         task_dir = output_dir / task_id
         task_dir.mkdir(exist_ok=True, parents=True)
-        logger.debug(f"task_id = '{task_id}' のアノテーション情報を '{task_dir}' ディレクトリに保存します。")
+        logger.debug(f"{logger_prefix}task_id = '{task_id}' のアノテーション情報を '{task_dir}' ディレクトリに保存します。")
+
+        is_failure = False
         for input_data_id in input_data_id_list:
-            self.dump_annotation_for_input_data(project_id, task_id, input_data_id, task_dir=task_dir)
+            try:
+                self.dump_annotation_for_input_data(task_id, input_data_id, task_dir=task_dir)
+            except Exception:
+                logger.warning(f"タスク'{task_id}', 入力データ'{input_data_id}' のアノテーション情報のダンプに失敗しました。", exc_info=True)
+                is_failure = True
+                continue
 
-        return True
+        return not is_failure
 
-    def dump_annotation(self, project_id: str, task_id_list: List[str], output_dir: Path):
-        super().validate_project(project_id, project_member_roles=None)
+    def dump_annotation_for_task_wrapper(self, tpl: tuple[int, str], output_dir: Path) -> bool:
+        task_index, task_id = tpl
+        try:
+            return self.dump_annotation_for_task(task_id, output_dir=output_dir, task_index=task_index)
+        except Exception:  # pylint: disable=broad-except
+            logger.warning(f"タスク'{task_id}'のアノテーション情報のダンプに失敗しました。", exc_info=True)
+            return False
 
-        project_title = self.facade.get_project_title(project_id)
+    def dump_annotation(self, task_id_list: List[str], output_dir: Path, parallelism: Optional[int] = None):
+        project_title = self.facade.get_project_title(self.project_id)
         logger.info(f"プロジェクト'{project_title}'に対して、タスク{len(task_id_list)} 件のアノテーションをファイルに保存します。")
 
         output_dir.mkdir(exist_ok=True, parents=True)
 
-        for task_id in task_id_list:
-            self.dump_annotation_for_task(project_id, task_id, output_dir=output_dir)
+        success_count = 0
 
-        logger.info(f"処理が完了しました。")
+        if parallelism is not None:
+            func = functools.partial(self.dump_annotation_for_task_wrapper, output_dir=output_dir)
+            with multiprocessing.Pool(parallelism) as pool:
+                result_bool_list = pool.map(func, enumerate(task_id_list))
+                success_count = len([e for e in result_bool_list if e])
+
+        else:
+            for task_index, task_id in enumerate(task_id_list):
+                try:
+                    result = self.dump_annotation_for_task(task_id, output_dir=output_dir, task_index=task_index)
+                    if result:
+                        success_count += 1
+                except Exception:
+                    logger.warning(f"タスク'{task_id}'のアノテーション情報のダンプに失敗しました。", exc_info=True)
+
+        logger.info(f"{success_count} / {len(task_id_list)} 件のタスクのアノテーション情報をダンプしました。")
+
+
+class DumpAnnotation(AbstractCommandLineInterface):
+    """
+    アノテーション情報をダンプする
+    """
 
     def main(self):
         args = self.args
         project_id = args.project_id
         task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id)
         output_dir = Path(args.output_dir)
-        self.dump_annotation(project_id, task_id_list, output_dir=output_dir)
+
+        super().validate_project(project_id, project_member_roles=None)
+
+        main_obj = DumpAnnotationMain(self.service, project_id)
+        main_obj.dump_annotation(task_id_list, output_dir=output_dir, parallelism=args.parallelism)
 
 
 def main(args):
@@ -110,6 +141,12 @@ def parse_args(parser: argparse.ArgumentParser):
 
     parser.add_argument("-o", "--output_dir", type=str, required=True, help="出力先ディレクトリのパス")
 
+    parser.add_argument(
+        "--parallelism",
+        type=int,
+        help="並列度。指定しない場合は、逐次的に処理します。",
+    )
+
     parser.set_defaults(subcommand_func=main)
 
 
diff --git a/annofabcli/annotation/import_annotation.py b/annofabcli/annotation/import_annotation.py
index 6fe17f9e..fbcc3951 100644
--- a/annofabcli/annotation/import_annotation.py
+++ b/annofabcli/annotation/import_annotation.py
@@ -1,11 +1,15 @@
+from __future__ import annotations
+
 import argparse
+import copy
 import logging
+import multiprocessing
 import sys
 import uuid
 import zipfile
 from dataclasses import dataclass
 from pathlib import Path
-from typing import Any, Dict, Iterator, List, Optional, Set, Union
+from typing import Any, Dict, Iterator, List, Optional, Union
 
 import annofabapi
 from annofabapi.dataclass.annotation import AdditionalData, AnnotationDetail
@@ -253,7 +257,6 @@ def _get_annotation_id(arg_label_info: LabelV1) -> str:
             with parser.open_outer_file(data_uri) as f:
                 s3_path = self.service.wrapper.upload_data_to_s3(self.project_id, f, content_type="image/png")
                 dest_obj.path = s3_path
-                logger.debug(f"{parser.task_id}/{parser.input_data_id}/{data_uri} をS3にアップロードしました。")
 
         return dest_obj
 
@@ -373,14 +376,13 @@ def put_annotation_for_task(self, task_parser: SimpleAnnotationParserByTask) ->
                     success_count += 1
             except Exception:  # pylint: disable=broad-except
                 logger.warning(
-                    f"task_id={parser.task_id}, input_data_id={parser.input_data_id} の" f"アノテーションインポートに失敗しました。",
+                    f"task_id={parser.task_id}, input_data_id={parser.input_data_id} の" f"アノテーションのインポートに失敗しました。",
                     exc_info=True,
                 )
 
-        logger.info(f"タスク'{task_parser.task_id}'の入力データ {success_count} 個に対してアノテーションをインポートしました。")
         return success_count
 
-    def execute_task(self, task_parser: SimpleAnnotationParserByTask) -> bool:
+    def execute_task(self, task_parser: SimpleAnnotationParserByTask, task_index: Optional[int] = None) -> bool:
         """
         1個のタスクに対してアノテーションを登録する。
 
@@ -395,7 +397,8 @@ def execute_task(self, task_parser: SimpleAnnotationParserByTask) -> bool:
         if not self.confirm_processing(f"task_id={task_id} のアノテーションをインポートしますか？"):
             return False
 
-        logger.info(f"task_id={task_id} に対して処理します。")
+        logger_prefix = f"{str(task_index+1)} 件目: " if task_index is not None else ""
+        logger.info(f"{logger_prefix}task_id={task_id} に対して処理します。")
 
         task = self.service.wrapper.get_task_or_none(self.project_id, task_id)
         if task is None:
@@ -411,11 +414,14 @@ def execute_task(self, task_parser: SimpleAnnotationParserByTask) -> bool:
         if self.is_force:
             if not can_put_annotation(task, self.service.api.account_id):
                 logger.debug(f"タスク'{task_id}' の担当者を自分自身に変更します。")
-                self.service.wrapper.change_task_operator(
-                    self.project_id, task_id, operator_account_id=self.service.api.account_id
+                old_account_id = task["account_id"]
+                task = self.service.wrapper.change_task_operator(
+                    self.project_id,
+                    task_id,
+                    operator_account_id=self.service.api.account_id,
+                    last_updated_datetime=task["updated_datetime"],
                 )
                 changed_operator = True
-                old_account_id = task["account_id"]
 
         else:
             if not can_put_annotation(task, self.service.api.account_id):
@@ -426,35 +432,76 @@ def execute_task(self, task_parser: SimpleAnnotationParserByTask) -> bool:
                 return False
 
         result_count = self.put_annotation_for_task(task_parser)
+        logger.info(f"{logger_prefix}タスク'{task_parser.task_id}'の入力データ {result_count} 個に対してアノテーションをインポートしました。")
+
         if changed_operator:
             logger.debug(f"タスク'{task_id}' の担当者を元に戻します。")
-            old_account_id = task["account_id"]
-            self.service.wrapper.change_task_operator(self.project_id, task_id, operator_account_id=old_account_id)
+            self.service.wrapper.change_task_operator(
+                self.project_id,
+                task_id,
+                operator_account_id=old_account_id,
+                last_updated_datetime=task["updated_datetime"],
+            )
 
         return result_count > 0
 
+    def execute_task_wrapper(
+        self,
+        tpl: tuple[int, SimpleAnnotationParserByTask],
+    ) -> bool:
+        task_index, task_parser = tpl
+        try:
+            return self.execute_task(task_parser, task_index=task_index)
+        except Exception:  # pylint: disable=broad-except
+            logger.warning(f"task_id={task_parser.task_id} のアノテーションのインポートに失敗しました。", exc_info=True)
+            return False
+
     def main(
         self,
         iter_task_parser: Iterator[SimpleAnnotationParserByTask],
-        target_task_ids: Optional[Set[str]] = None,
+        target_task_ids: Optional[set[str]] = None,
+        parallelism: Optional[int] = None,
     ):
+        def get_iter_task_parser_from_task_ids(
+            _iter_task_parser: Iterator[SimpleAnnotationParserByTask], _target_task_ids: set[str]
+        ) -> Iterator[SimpleAnnotationParserByTask]:
+            for task_parser in _iter_task_parser:
+                if task_parser.task_id in _target_task_ids:
+                    _target_task_ids.remove(task_parser.task_id)
+                    yield task_parser
+
+        if target_task_ids is not None:
+            # コマンドライン引数で --task_idが指定された場合は、対象のタスクのみインポートする
+            # tmp_target_task_idsが関数内で変更されるので、事前にコピーする
+            tmp_target_task_ids = copy.deepcopy(target_task_ids)
+            iter_task_parser = get_iter_task_parser_from_task_ids(iter_task_parser, tmp_target_task_ids)
+
         success_count = 0
-        for task_parser in iter_task_parser:
-            try:
-                if target_task_ids is not None and len(target_task_ids) > 0:
-                    # コマンドライン引数で --task_idが指定された場合は、対象のタスクのみインポートする
-                    if task_parser.task_id in target_task_ids:
-                        if self.execute_task(task_parser):
-                            success_count += 1
-                else:
-                    # コマンドライン引数で --task_idが指定されていない場合はすべてをインポートする
-                    if self.execute_task(task_parser):
-                        success_count += 1
+        task_count = 0
+        if parallelism is not None:
+            with multiprocessing.Pool(parallelism) as pool:
+                result_bool_list = pool.map(self.execute_task_wrapper, enumerate(iter_task_parser))
+                success_count = len([e for e in result_bool_list if e])
+                task_count = len(result_bool_list)
 
-            except Exception:  # pylint: disable=broad-except
-                logger.warning(f"task_id={task_parser.task_id} のアノテーションインポートに失敗しました。", exc_info=True)
+        else:
+            for task_index, task_parser in enumerate(iter_task_parser):
+                try:
+                    result = self.execute_task(task_parser, task_index=task_index)
+                    if result:
+                        success_count += 1
+                except Exception:
+                    logger.warning(f"task_id={task_parser.task_id} のアノテーションのインポートに失敗しました。", exc_info=True)
+                    continue
+                finally:
+                    task_count += 1
+
+        if target_task_ids is not None and len(tmp_target_task_ids) > 0:
+            logger.warning(
+                f"'--task_id'で指定したタスクの内 {len(tmp_target_task_ids)} 件は、インポート対象のアノテーションデータに含まれていません。 :: {tmp_target_task_ids}"  # noqa: E501
+            )
 
-        logger.info(f"{success_count} 個のタスクに対してアノテーションをインポートしました。")
+        logger.info(f"{success_count} / {task_count} 件のタスクに対してアノテーションをインポートしました。")
 
 
 class ImportAnnotation(AbstractCommandLineInterface):
@@ -477,10 +524,19 @@ def validate(args: argparse.Namespace) -> bool:
             )
             return False
 
-        elif annotation_path.is_file() and not zipfile.is_zipfile(str(annotation_path)):
+        elif (annotation_path.is_file() and not zipfile.is_zipfile(str(annotation_path))) or (
+            not annotation_path.is_dir()
+        ):
             print(f"{COMMON_MESSAGE} argument --annotation: ZIPファイルまたはディレクトリを指定してください。", file=sys.stderr)
             return False
 
+        if args.parallelism is not None and not args.yes:
+            print(
+                f"{COMMON_MESSAGE} argument --parallelism: '--parallelism'を指定するときは、必ず '--yes' を指定してください。",
+                file=sys.stderr,
+            )
+            return False
+
         return True
 
     def main(self):
@@ -489,7 +545,7 @@ def main(self):
             sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
 
         project_id = args.project_id
-        annotation_path = Path(args.annotation)
+        annotation_path: Path = args.annotation
 
         super().validate_project(project_id, [ProjectMemberRole.OWNER])
 
@@ -515,7 +571,7 @@ def main(self):
             is_force=args.force,
         )
 
-        main_obj.main(iter_task_parser, target_task_ids=target_task_ids)
+        main_obj.main(iter_task_parser, target_task_ids=target_task_ids, parallelism=args.parallelism)
 
 
 def main(args):
@@ -531,7 +587,7 @@ def parse_args(parser: argparse.ArgumentParser):
 
     parser.add_argument(
         "--annotation",
-        type=str,
+        type=Path,
         required=True,
         help="Simpleアノテーションと同じフォルダ構成のzipファイル or ディレクトリのパスを指定してください。" "タスクの状態が作業中/完了の場合はインポートしません。",
     )
@@ -558,6 +614,12 @@ def parse_args(parser: argparse.ArgumentParser):
         "--force", action="store_true", help="過去に割り当てられていて現在の担当者が自分自身でない場合、タスクの担当者を自分自身に変更してからアノテーションをインポートします。"
     )
 
+    parser.add_argument(
+        "--parallelism",
+        type=int,
+        help="並列度。指定しない場合は、逐次的に処理します。指定した場合は、``--yes`` も指定してください。",
+    )
+
     parser.set_defaults(subcommand_func=main)
 
 
diff --git a/annofabcli/annotation/restore_annotation.py b/annofabcli/annotation/restore_annotation.py
index b7f15611..8a801e7c 100644
--- a/annofabcli/annotation/restore_annotation.py
+++ b/annofabcli/annotation/restore_annotation.py
@@ -1,8 +1,14 @@
+from __future__ import annotations
+
 import argparse
+import copy
 import logging
+import multiprocessing
+import sys
 from pathlib import Path
-from typing import Any, Dict, List, Optional
+from typing import Any, Dict, Iterator, List, Optional
 
+import annofabapi
 from annofabapi.dataclass.annotation import Annotation, AnnotationDetail
 from annofabapi.models import AnnotationDataHoldingType, ProjectMemberRole, TaskStatus
 from annofabapi.parser import (
@@ -14,18 +20,34 @@
 
 import annofabcli
 from annofabcli import AnnofabApiFacade
-from annofabcli.common.cli import AbstractCommandLineInterface, ArgumentParser, build_annofabapi_resource_and_login
+from annofabcli.common.cli import (
+    COMMAND_LINE_ERROR_STATUS_CODE,
+    AbstractCommandLineInterface,
+    AbstractCommandLineWithConfirmInterface,
+    ArgumentParser,
+    build_annofabapi_resource_and_login,
+)
 
 logger = logging.getLogger(__name__)
 
 
-class RestoreAnnotation(AbstractCommandLineInterface):
-    """
-    アノテーションをリストアする。
-    """
+class RestoreAnnotationMain(AbstractCommandLineWithConfirmInterface):
+    def __init__(
+        self,
+        service: annofabapi.Resource,
+        *,
+        project_id: str,
+        is_force: bool,
+        all_yes: bool,
+    ):
+        self.service = service
+        AbstractCommandLineWithConfirmInterface.__init__(self, all_yes)
+
+        self.project_id = project_id
+        self.is_force = is_force
 
     def _to_annotation_detail_for_request(
-        self, project_id: str, parser: SimpleAnnotationParser, detail: AnnotationDetail
+        self, parser: SimpleAnnotationParser, detail: AnnotationDetail
     ) -> AnnotationDetail:
         """
         Request Bodyに渡すDataClassに変換する。塗りつぶし画像があれば、それをS3にアップロードする。
@@ -45,27 +67,26 @@ def _to_annotation_detail_for_request(
 
             if data_uri is not None:
                 with parser.open_outer_file(data_uri) as f:
-                    s3_path = self.service.wrapper.upload_data_to_s3(project_id, f, content_type="image/png")
+                    s3_path = self.service.wrapper.upload_data_to_s3(self.project_id, f, content_type="image/png")
                     detail.path = s3_path
-                    logger.debug(f"{parser.task_id}/{parser.input_data_id}/{data_uri} をS3にアップロードしました。")
             else:
                 logger.warning(f"annotation_id={detail.annotation_id}: data_holding_typeが'outer'なのにpathがNoneです。")
 
         return detail
 
-    def parser_to_request_body(self, project_id: str, parser: SimpleAnnotationParser) -> Dict[str, Any]:
+    def parser_to_request_body(self, parser: SimpleAnnotationParser) -> Dict[str, Any]:
         # infer_missing=Trueを指定する理由：Optional型のキーが存在しない場合でも、Annotationデータクラスのインスタンスを生成できるようにするため
         # https://qiita.com/yuji38kwmt/items/c5b56f70da3b8a70ba31
         annotation: Annotation = Annotation.from_dict(parser.load_json(), infer_missing=True)
         request_details: List[Dict[str, Any]] = []
         for detail in annotation.details:
-            request_detail = self._to_annotation_detail_for_request(project_id, parser, detail)
+            request_detail = self._to_annotation_detail_for_request(parser, detail)
 
             if request_detail is not None:
                 request_details.append(request_detail.to_dict(encode_json=True))
 
         request_body = {
-            "project_id": project_id,
+            "project_id": self.project_id,
             "task_id": parser.task_id,
             "input_data_id": parser.input_data_id,
             "details": request_details,
@@ -73,29 +94,29 @@ def parser_to_request_body(self, project_id: str, parser: SimpleAnnotationParser
 
         return request_body
 
-    def put_annotation_for_input_data(self, project_id: str, parser: SimpleAnnotationParser) -> bool:
+    def put_annotation_for_input_data(self, parser: SimpleAnnotationParser) -> bool:
 
         task_id = parser.task_id
         input_data_id = parser.input_data_id
 
-        old_annotation, _ = self.service.api.get_editor_annotation(project_id, task_id, input_data_id)
+        old_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id)
 
         logger.info(f"task_id={task_id}, input_data_id={input_data_id} : アノテーションをリストアします。")
-        request_body = self.parser_to_request_body(project_id, parser)
+        request_body = self.parser_to_request_body(parser)
 
         updated_datetime = old_annotation["updated_datetime"] if old_annotation is not None else None
         request_body["updated_datetime"] = updated_datetime
-        self.service.api.put_annotation(project_id, task_id, input_data_id, request_body=request_body)
+        self.service.api.put_annotation(self.project_id, task_id, input_data_id, request_body=request_body)
         return True
 
-    def put_annotation_for_task(self, project_id: str, task_parser: SimpleAnnotationParserByTask) -> int:
+    def put_annotation_for_task(self, task_parser: SimpleAnnotationParserByTask) -> int:
 
         logger.info(f"タスク'{task_parser.task_id}' のアノテーションをリストアします。")
 
         success_count = 0
         for parser in task_parser.lazy_parse():
             try:
-                if self.put_annotation_for_input_data(project_id, parser):
+                if self.put_annotation_for_input_data(parser):
                     success_count += 1
             except Exception:  # pylint: disable=broad-except
                 logger.warning(
@@ -103,29 +124,29 @@ def put_annotation_for_task(self, project_id: str, task_parser: SimpleAnnotation
                     exc_info=True,
                 )
 
-        logger.info(f"タスク'{task_parser.task_id}'の入力データ {success_count} 個に対してアノテーションをリストアしました。")
         return success_count
 
-    def execute_task(self, project_id: str, task_parser: SimpleAnnotationParserByTask, force: bool) -> bool:
+    def execute_task(self, task_parser: SimpleAnnotationParserByTask, task_index: Optional[int] = None) -> bool:
         """
         1個のタスクに対してアノテーションを登録する。
 
         Args:
-            project_id:
-            task_parser:
-            force:
+            task_parser: タスクをパースするためのインスタンス。
+                Simpleアノテーションではないのに型が`SimpleAnnotationParserByTask`である理由：SimpleAnnotationParserByTaskのメソッドを利用するため
+            task_index: タスクのインデックス
 
         Returns:
             1個以上の入力データのアノテーションを変更したか
 
         """
+        logger_prefix = f"{str(task_index+1)} 件目: " if task_index is not None else ""
         task_id = task_parser.task_id
         if not self.confirm_processing(f"task_id={task_id} のアノテーションをリストアしますか？"):
             return False
 
-        logger.info(f"task_id={task_id} に対して処理します。")
+        logger.info(f"{logger_prefix}task_id={task_id} に対して処理します。")
 
-        task = self.service.wrapper.get_task_or_none(project_id, task_id)
+        task = self.service.wrapper.get_task_or_none(self.project_id, task_id)
         if task is None:
             logger.warning(f"task_id = '{task_id}' は存在しません。")
             return False
@@ -136,14 +157,17 @@ def execute_task(self, project_id: str, task_parser: SimpleAnnotationParserByTas
 
         old_account_id: Optional[str] = None
         changed_operator = False
-        if force:
+        if self.is_force:
             if not can_put_annotation(task, self.service.api.account_id):
                 logger.debug(f"タスク'{task_id}' の担当者を自分自身に変更します。")
-                self.service.wrapper.change_task_operator(
-                    project_id, task_id, operator_account_id=self.service.api.account_id
+                old_account_id = task["account_id"]
+                task = self.service.wrapper.change_task_operator(
+                    self.project_id,
+                    task_id,
+                    operator_account_id=self.service.api.account_id,
+                    last_updated_datetime=task["updated_datetime"],
                 )
                 changed_operator = True
-                old_account_id = task["account_id"]
 
         else:
             if not can_put_annotation(task, self.service.api.account_id):
@@ -153,43 +177,123 @@ def execute_task(self, project_id: str, task_parser: SimpleAnnotationParserByTas
                 )
                 return False
 
-        result_count = self.put_annotation_for_task(project_id, task_parser)
+        result_count = self.put_annotation_for_task(task_parser)
+        logger.info(f"{logger_prefix}タスク'{task_parser.task_id}'の入力データ {result_count} 個に対してアノテーションをリストアしました。")
+
         if changed_operator:
             logger.debug(f"タスク'{task_id}' の担当者を元に戻します。")
-            old_account_id = task["account_id"]
-            self.service.wrapper.change_task_operator(project_id, task_id, operator_account_id=old_account_id)
+            self.service.wrapper.change_task_operator(
+                self.project_id,
+                task_id,
+                operator_account_id=old_account_id,
+                last_updated_datetime=task["updated_datetime"],
+            )
 
         return result_count > 0
 
-    def main(self):
-        args = self.args
-        project_id = args.project_id
-        annotation_dir_path = Path(args.annotation)
+    def execute_task_wrapper(
+        self,
+        tpl: tuple[int, SimpleAnnotationParserByTask],
+    ) -> bool:
+        task_index, task_parser = tpl
+        try:
+            return self.execute_task(task_parser, task_index=task_index)
+        except Exception:  # pylint: disable=broad-except
+            logger.warning(f"task_id={task_parser.task_id} のアノテーションのリストアに失敗しました。", exc_info=True)
+            return False
 
-        super().validate_project(project_id, [ProjectMemberRole.OWNER])
+    def main(
+        self,
+        annotation_dir: Path,
+        target_task_ids: Optional[set[str]] = None,
+        parallelism: Optional[int] = None,
+    ):
+        """`annotation_dir`にあるファイルからアノテーションをリストアします。
+
+        Args:
+            annotation_dir (Path): `annofabcli annotation dump`で出力したディレクトリ
+            target_task_ids: リストア対象のtask_id
+            parallelism: 並列度。Noneなら逐次処理
+        """
 
-        task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id)
+        def get_iter_task_parser_from_task_ids(
+            _iter_task_parser: Iterator[SimpleAnnotationParserByTask], _target_task_ids: set[str]
+        ) -> Iterator[SimpleAnnotationParserByTask]:
+            for task_parser in _iter_task_parser:
+                if task_parser.task_id in _target_task_ids:
+                    _target_task_ids.remove(task_parser.task_id)
+                    yield task_parser
 
-        # dumpしたアノテーションディレクトリの読み込み
-        iter_task_parser = lazy_parse_simple_annotation_dir_by_task(annotation_dir_path)
+        # Simpleアノテーションではないのに`lazy_parse_simple_annotation_dir_by_task`を実行した理由：SimpleAnnotationParseに関する関数を利用するため
+        iter_task_parser = lazy_parse_simple_annotation_dir_by_task(annotation_dir)
+
+        if target_task_ids is not None:
+            # コマンドライン引数で --task_idが指定された場合は、対象のタスクのみインポートする
+            # tmp_target_task_idsが関数内で変更されるので、事前にコピーする
+            tmp_target_task_ids = copy.deepcopy(target_task_ids)
+            iter_task_parser = get_iter_task_parser_from_task_ids(iter_task_parser, tmp_target_task_ids)
 
         success_count = 0
-        for task_parser in iter_task_parser:
-            try:
-                if len(task_id_list) > 0:
-                    # コマンドライン引数で --task_idが指定された場合は、対象のタスクのみリストアする
-                    if task_parser.task_id in task_id_list:
-                        if self.execute_task(project_id, task_parser, force=args.force):
-                            success_count += 1
-                else:
-                    # コマンドライン引数で --task_idが指定されていない場合はすべてをリストアする
-                    if self.execute_task(project_id, task_parser, force=args.force):
+        task_count = 0
+        if parallelism is not None:
+            with multiprocessing.Pool(parallelism) as pool:
+                result_bool_list = pool.map(self.execute_task_wrapper, enumerate(iter_task_parser))
+                success_count = len([e for e in result_bool_list if e])
+                task_count = len(result_bool_list)
+
+        else:
+            for task_index, task_parser in enumerate(iter_task_parser):
+                try:
+                    result = self.execute_task(task_parser, task_index=task_index)
+                    if result:
                         success_count += 1
+                except Exception:
+                    logger.warning(f"task_id={task_parser.task_id} のアノテーションのリストアに失敗しました。", exc_info=True)
+                    continue
+                finally:
+                    task_count += 1
 
-            except Exception:  # pylint: disable=broad-except
-                logger.warning(f"task_id={task_parser.task_id} のアノテーションのリストアに失敗しました。", exc_info=True)
+        if target_task_ids is not None and len(tmp_target_task_ids) > 0:
+            logger.warning(
+                f"'--task_id'で指定したタスクの内 {len(tmp_target_task_ids)} 件は、リストア対象のアノテーションデータに含まれていません。 :: {tmp_target_task_ids}"  # noqa: E501
+            )
 
-        logger.info(f"{success_count} 個のタスクに対してアノテーションをリストアしました。")
+        logger.info(f"{success_count} / {task_count} 件のタスクに対してアノテーションをリストアしました。")
+
+
+class RestoreAnnotation(AbstractCommandLineInterface):
+    """
+    アノテーションをリストアする。
+    """
+
+    COMMON_MESSAGE = "annofabcli annotation restore: error:"
+
+    def validate(self, args: argparse.Namespace) -> bool:
+
+        if args.parallelism is not None and not args.yes:
+            print(
+                f"{self.COMMON_MESSAGE} argument --parallelism: '--parallelism'を指定するときは、必ず '--yes' を指定してください。",
+                file=sys.stderr,
+            )
+            return False
+
+        return True
+
+    def main(self):
+        args = self.args
+
+        if not self.validate(args):
+            sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
+
+        project_id = args.project_id
+
+        super().validate_project(project_id, [ProjectMemberRole.OWNER])
+
+        task_id_list = set(annofabcli.common.cli.get_list_from_args(args.task_id)) if args.task_id is not None else None
+
+        RestoreAnnotationMain(self.service, project_id=project_id, is_force=args.force, all_yes=args.yes).main(
+            args.annotation, target_task_ids=task_id_list, parallelism=args.parallelism
+        )
 
 
 def main(args):
@@ -205,7 +309,7 @@ def parse_args(parser: argparse.ArgumentParser):
 
     parser.add_argument(
         "--annotation",
-        type=str,
+        type=Path,
         required=True,
         help="'annotation dump'コマンドの保存先ディレクトリのパスを指定してください。",
     )
@@ -216,6 +320,12 @@ def parse_args(parser: argparse.ArgumentParser):
         "--force", action="store_true", help="過去に割り当てられていて現在の担当者が自分自身でない場合、タスクの担当者を自分自身に変更してからアノテーションをリストアします。"
     )
 
+    parser.add_argument(
+        "--parallelism",
+        type=int,
+        help="並列度。指定しない場合は、逐次的に処理します。指定した場合は、``--yes`` も指定してください。",
+    )
+
     parser.set_defaults(subcommand_func=main)
 
 
diff --git a/annofabcli/common/facade.py b/annofabcli/common/facade.py
index 5c7e8b6f..f5b3051f 100644
--- a/annofabcli/common/facade.py
+++ b/annofabcli/common/facade.py
@@ -495,6 +495,10 @@ def contains_any_organization_member_role(
         else:
             return False
 
+    ##################
+    # operateTaskのfacade
+    ##################
+
     @staticmethod
     def get_label_info_from_name(annotation_specs_labels: List[Dict[str, Any]], label_name_en: str) -> Dict[str, Any]:
         labels = [e for e in annotation_specs_labels if AnnofabApiFacade.get_label_name_en(e) == label_name_en]
@@ -700,76 +704,8 @@ def get_annotation_list_for_task(
             dict_query.update(asdict(query))
         query_params = {"query": dict_query}
         annotation_list = self.service.wrapper.get_all_annotation_list(project_id, query_params=query_params)
-        assert all(e["task_id"] == task_id for e in annotation_list), f"task_id='{task_id}' 以外のアノテーションが取得されています！！"
         return annotation_list
 
-    def delete_annotation_list(
-        self, project_id: str, annotation_list: List[SingleAnnotation]
-    ) -> Optional[List[Dict[str, Any]]]:
-        """
-        アノテーション一覧を削除する。
-        【注意】取扱注意
-
-        Args:
-            project_id:
-            annotation_list: アノテーション一覧
-
-        Returns:
-            `batch_update_annotations`メソッドのレスポンス
-
-        """
-
-        def _to_request_body_elm(annotation: Dict[str, Any]) -> Dict[str, Any]:
-            detail = annotation["detail"]
-            return {
-                "project_id": annotation["project_id"],
-                "task_id": annotation["task_id"],
-                "input_data_id": annotation["input_data_id"],
-                "updated_datetime": annotation["updated_datetime"],
-                "annotation_id": detail["annotation_id"],
-                "_type": "Delete",
-            }
-
-        request_body = [_to_request_body_elm(annotation) for annotation in annotation_list]
-        return self.service.api.batch_update_annotations(project_id, request_body)[0]
-
-    def change_annotation_attributes(
-        self, project_id: str, annotation_list: List[SingleAnnotation], attributes: List[AdditionalData]
-    ) -> Optional[List[Dict[str, Any]]]:
-        """
-        アノテーション属性値を変更する。
-
-        【注意】取扱注意
-
-        Args:
-            project_id:
-            annotation_list: 変更対象のアノテーション一覧
-            attributes: 変更後の属性値
-
-        Returns:
-            `batch_update_annotations`メソッドのレスポンス
-
-        """
-
-        def _to_request_body_elm(annotation: Dict[str, Any]) -> Dict[str, Any]:
-            detail = annotation["detail"]
-            return {
-                "data": {
-                    "project_id": annotation["project_id"],
-                    "task_id": annotation["task_id"],
-                    "input_data_id": annotation["input_data_id"],
-                    "updated_datetime": annotation["updated_datetime"],
-                    "annotation_id": detail["annotation_id"],
-                    "label_id": detail["label_id"],
-                    "additional_data_list": attributes_for_dict,
-                },
-                "_type": "Put",
-            }
-
-        attributes_for_dict: List[Dict[str, Any]] = [asdict(e) for e in attributes]
-        request_body = [_to_request_body_elm(annotation) for annotation in annotation_list]
-        return self.service.api.batch_update_annotations(project_id, request_body)[0]
-
     def set_account_id_of_task_query(self, project_id: str, task_query: TaskQuery) -> TaskQuery:
         """
         タスククエリ条件のuser_idの値をaccount_idに設定する。
diff --git a/annofabcli/common/utils.py b/annofabcli/common/utils.py
index 1e43bbcf..23cceea1 100644
--- a/annofabcli/common/utils.py
+++ b/annofabcli/common/utils.py
@@ -23,10 +23,10 @@
 
 def read_lines(filepath: str) -> List[str]:
     """ファイルを行単位で読み込む。改行コードを除く"""
-    with open(filepath, encoding="utf-8") as f:
+    # BOM付きUTF-8のファイルも読み込めるようにする
+    # annofabcliが出力するCSVはデフォルトでBOM付きUTF-8。これを加工してannofabcliに読み込ませる場合もあるので、BOM付きUTF-8に対応させた
+    with open(filepath, encoding="utf-8-sig") as f:
         lines = f.readlines()
-    # BOMがあれば除去する
-    lines[0] = lines[0].lstrip("\ufeff")
     return [e.rstrip("\r\n") for e in lines]
 
 
diff --git a/annofabcli/task/cancel_acceptance.py b/annofabcli/task/cancel_acceptance.py
index e9cf84e4..0027bfbf 100644
--- a/annofabcli/task/cancel_acceptance.py
+++ b/annofabcli/task/cancel_acceptance.py
@@ -214,7 +214,7 @@ def validate(cls, args: argparse.Namespace) -> bool:
 
         if args.parallelism is not None and not args.yes:
             print(
-                f"{cls.COMMON_MESSAGE} argument --parallelism: '--parallelism'を指定するときは、必ず ``--yes`` を指定してください。",
+                f"{cls.COMMON_MESSAGE} argument --parallelism: '--parallelism'を指定するときは、'--yes' を指定してください。",
                 file=sys.stderr,
             )
             return False
diff --git a/annofabcli/task/complete_tasks.py b/annofabcli/task/complete_tasks.py
index d93ca66b..86a0735d 100644
--- a/annofabcli/task/complete_tasks.py
+++ b/annofabcli/task/complete_tasks.py
@@ -296,38 +296,6 @@ def complete_task_for_annotation_phase(
         self.service.wrapper.complete_task(task.project_id, task.task_id, last_updated_datetime=task.updated_datetime)
         logger.info(f"{task.task_id}: 教師付フェーズをフェーズに進めました。")
         return True
-        # if unanswered_comment_count_for_task == 0:
-        #     if not self.confirm_processing(f"タスク'{task.task_id}'の教師付フェーズを次のフェーズに進めますか？"):
-        #         return False
-
-        #     self.change_to_working_status(task)
-        #     self.service.wrapper.complete_task(task.project_id, task.task_id)
-        #     logger.info(f"{task.task_id}: 教師付フェーズを次のフェーズに進めました。")
-        #     return True
-        # else:
-        #     logger.debug(f"{task.task_id}: 未回答の検査コメントが {unanswered_comment_count_for_task} 件あります。")
-        #     if reply_comment is None:
-        #         logger.warning(f"{task.task_id}: 未回答の検査コメントに対する返信コメント（'--reply_comment'）が指定されていないので、スキップします。")
-        #         return False
-        #     elif not self.confirm_processing(f"タスク'{task.task_id}'の教師付フェーズを次のフェーズに進めますか？"):
-        #         return False
-        #     else:
-        #         changed_task = self.change_to_working_status(task)
-
-        #         logger.debug(f"{task.task_id}: 未回答の検査コメント {unanswered_comment_count_for_task} 件に対して、返信コメントを付与します。")
-        #         for input_data_id, unanswered_comment_list in unanswered_comment_list_dict.items():
-        #             if len(unanswered_comment_list) == 0:
-        #                 continue
-        #             self.reply_inspection_comment(
-        #                 changed_task,
-        #                 input_data_id=input_data_id,
-        #                 unanswered_comment_list=unanswered_comment_list,
-        #                 reply_comment=reply_comment,
-        #             )
-
-        #         self.service.wrapper.complete_task(task.project_id, task.task_id)
-        #         logger.info(f"{task.task_id}: 教師付フェーズをフェーズに進めました。")
-        #         return True
 
     def complete_task_for_inspection_acceptance_phase(
         self,
@@ -373,41 +341,6 @@ def complete_task_for_inspection_acceptance_phase(
         logger.info(f"{task.task_id}: 検査/受入フェーズを次のフェーズに進めました。")
         return True
 
-        # if unprocessed_inspection_count == 0:
-        #     if not self.confirm_processing(f"タスク'{task.task_id}'の検査/受入フェーズを次のフェーズに進めますか？"):
-        #         return False
-
-        #     self.change_to_working_status(task)
-        #     self.service.wrapper.complete_task(task.project_id, task.task_id)
-        #     logger.info(f"{task.task_id}: 検査/受入フェーズを次のフェーズに進めました。")
-        #     return True
-
-        # else:
-        #     logger.debug(f"{task.task_id}: 未処置の検査コメントが {unprocessed_inspection_count} 件あります。")
-        #     if inspection_status is None:
-        #         logger.warning(f"{task.task_id}: 未処置の検査コメントに対する対応方法（'--inspection_status'）が指定されていないので、スキップします。")
-        #         return False
-        #     elif not self.confirm_processing(f"タスク'{task.task_id}'の検査/受入フェーズを次のフェーズに進めますか？"):
-        #         return False
-
-        #     changed_task = self.change_to_working_status(task)
-
-        #     logger.debug(f"{task.task_id}: 未処置の検査コメントを、{inspection_status.value} 状態にします。")
-        #     for input_data_id, unprocessed_inspection_list in unprocessed_inspection_list_dict.items():
-        #         if len(unprocessed_inspection_list) == 0:
-        #             continue
-
-        #         self.update_status_of_inspections(
-        #             changed_task,
-        #             input_data_id,
-        #             comment_list=unprocessed_inspection_list,
-        #             comment_status=inspection_status,
-        #         )
-
-        #     self.service.wrapper.complete_task(task.project_id, task.task_id)
-        #     logger.info(f"{task.task_id}: 検査/受入フェーズを次のフェーズに進めました。")
-        #     return True
-
     @staticmethod
     def _validate_task(
         task: Task, target_phase: TaskPhase, target_phase_stage: int, task_query: Optional[TaskQuery]
@@ -579,7 +512,7 @@ def validate(args: argparse.Namespace) -> bool:
 
         if args.parallelism is not None and not args.yes:
             print(
-                f"{COMMON_MESSAGE} argument --parallelism: '--parallelism'を指定するときは、必ず ``--yes`` を指定してください。",
+                f"{COMMON_MESSAGE} argument --parallelism: '--parallelism'を指定するときは、'--yes' を指定してください。",
                 file=sys.stderr,
             )
             return False
diff --git a/annofabcli/task/copy_tasks.py b/annofabcli/task/copy_tasks.py
index 11f32aba..a96b3c5b 100644
--- a/annofabcli/task/copy_tasks.py
+++ b/annofabcli/task/copy_tasks.py
@@ -153,9 +153,23 @@ def main(self, project_id: str, copy_target_list: list[CopyTarget], parallelism:
 class CopyTasks(AbstractCommandLineInterface):
     COMMON_MESSAGE = "annofabcli task copy: error:"
 
+    def validate(self, args: argparse.Namespace) -> bool:
+
+        if args.parallelism is not None and not args.yes:
+            print(
+                f"{self.COMMON_MESSAGE} argument --parallelism: '--parallelism'を指定するときは、必ず '--yes' を指定してください。",
+                file=sys.stderr,
+            )
+            return False
+
+        return True
+
     def main(self):
         args = self.args
 
+        if not self.validate(args):
+            sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
+
         str_copy_target_list = annofabcli.common.cli.get_list_from_args(args.input)
         copy_target_list = get_copy_target_list(str_copy_target_list)
         if len(str_copy_target_list) != len(copy_target_list):
@@ -170,7 +184,7 @@ def main(self):
             all_yes=self.all_yes,
             is_copy_metadata=args.copy_metadata,
         )
-        main_obj.main(project_id, copy_target_list=copy_target_list)
+        main_obj.main(project_id, copy_target_list=copy_target_list, parallelism=args.parallelism)
 
 
 def main(args: argparse.Namespace):
diff --git a/annofabcli/task/reject_tasks.py b/annofabcli/task/reject_tasks.py
index 4e17ebc8..b6ee955f 100644
--- a/annofabcli/task/reject_tasks.py
+++ b/annofabcli/task/reject_tasks.py
@@ -369,7 +369,7 @@ def validate(self, args: argparse.Namespace) -> bool:
 
         if args.parallelism is not None and not args.yes:
             print(
-                f"{self.COMMON_MESSAGE} argument --parallelism: '--parallelism'を指定するときは、必ず ``--yes`` を指定してください。",
+                f"{self.COMMON_MESSAGE} argument --parallelism: '--parallelism'を指定するときは、'--yes' を指定してください。",
                 file=sys.stderr,
             )
             return False
diff --git a/annofabcli/task/update_metadata_of_task.py b/annofabcli/task/update_metadata_of_task.py
index 4828c000..5e45ef15 100644
--- a/annofabcli/task/update_metadata_of_task.py
+++ b/annofabcli/task/update_metadata_of_task.py
@@ -183,7 +183,7 @@ def validate(args: argparse.Namespace) -> bool:
 
         if args.parallelism is not None and not args.yes:
             print(
-                f"{COMMON_MESSAGE} argument --parallelism: '--parallelism' を指定するときは、必ず  ``--yes``  を指定してください。",
+                f"{COMMON_MESSAGE} argument --parallelism: '--parallelism' を指定するときは、必ず  '--yes'  を指定してください。",
                 file=sys.stderr,
             )
             return False
diff --git a/docs/command_reference/annotation/list.rst b/docs/command_reference/annotation/list.rst
index 0730cc0a..a525e3cb 100644
--- a/docs/command_reference/annotation/list.rst
+++ b/docs/command_reference/annotation/list.rst
@@ -30,7 +30,7 @@ Examples
     WebAPIの都合上、集計対象のアノテーションは10,000個までしか検索できません。
 
 
-``--annotation_query`` で、アノテーションを検索するする条件を指定することができます。。フォーマットは https://annofab.com/docs/api/#section/AnnotationQuery とほとんど同じです。
+``--annotation_query`` で、アノテーションを検索するする条件を指定することができます。フォーマットは https://annofab.com/docs/api/#section/AnnotationQuery とほとんど同じです。
 さらに追加で ``label_name_en`` , ``additional_data_definition_name_en`` , ``choice_name_en`` キーも指定できます。``label_id`` または ``label_name_en`` のいずれかは必ず指定してください。
 
 ``--annotation_query`` のサンプルは、`Command line options <../../user_guide/command_line_options.html#annotation-query-aq>`_ を参照してください。
diff --git a/poetry.lock b/poetry.lock
index 0527ed3f..dc412ad7 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -16,7 +16,7 @@ python-versions = "*"
 
 [[package]]
 name = "annofabapi"
-version = "0.57.0"
+version = "0.60.2"
 description = "Python Clinet Library of AnnoFab WebAPI (https://annofab.com/docs/api/)"
 category = "main"
 optional = false
@@ -25,6 +25,7 @@ python-versions = ">=3.7,<4.0"
 [package.dependencies]
 backoff = "*"
 dataclasses-json = "*"
+more-itertools = "*"
 python-dateutil = "*"
 requests = "*"
 
@@ -206,7 +207,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
 [[package]]
 name = "coverage"
-version = "6.3.2"
+version = "6.3.3"
 description = "Code coverage measurement for Python"
 category = "dev"
 optional = false
@@ -1255,7 +1256,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.7.1"
-content-hash = "9baa3e66b3a8372df84853beac447cab1b373a33869499621bd8595daa8358c0"
+content-hash = "dc02b496f6733652da45dc8890f27b6d239962d64d2f90abd1d2811796b8c21b"
 
 [metadata.files]
 alabaster = [
@@ -1267,8 +1268,8 @@ altgraph = [
     {file = "altgraph-0.17.2.tar.gz", hash = "sha256:ebf2269361b47d97b3b88e696439f6e4cbc607c17c51feb1754f90fb79839158"},
 ]
 annofabapi = [
-    {file = "annofabapi-0.57.0-py3-none-any.whl", hash = "sha256:ea58975e836e4c6fc7da24c2173b57af04f99d8ab91760036a5d4b0a4ffb4e57"},
-    {file = "annofabapi-0.57.0.tar.gz", hash = "sha256:f75b2d4d19cfa4e8eab265c949e2aafbe5e9242cc07d1d1b95ca1c12763c98fd"},
+    {file = "annofabapi-0.60.2-py3-none-any.whl", hash = "sha256:a5164a15f14b20556882741e7d5f631723484a3d948c3fcf443dcfc191784a37"},
+    {file = "annofabapi-0.60.2.tar.gz", hash = "sha256:5c5ddaf56967d1a29396af86e978a971b0392b4ee8730d58b5e60463f559c4e3"},
 ]
 appnope = [
     {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"},
@@ -1351,47 +1352,47 @@ colorama = [
     {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
 ]
 coverage = [
-    {file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"},
-    {file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"},
-    {file = "coverage-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1"},
-    {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4"},
-    {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903"},
-    {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c"},
-    {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f"},
-    {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05"},
-    {file = "coverage-6.3.2-cp310-cp310-win32.whl", hash = "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39"},
-    {file = "coverage-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1"},
-    {file = "coverage-6.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa"},
-    {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518"},
-    {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7"},
-    {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6"},
-    {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad"},
-    {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359"},
-    {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4"},
-    {file = "coverage-6.3.2-cp37-cp37m-win32.whl", hash = "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca"},
-    {file = "coverage-6.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3"},
-    {file = "coverage-6.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d"},
-    {file = "coverage-6.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059"},
-    {file = "coverage-6.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512"},
-    {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca"},
-    {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d"},
-    {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0"},
-    {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6"},
-    {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"},
-    {file = "coverage-6.3.2-cp38-cp38-win32.whl", hash = "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e"},
-    {file = "coverage-6.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1"},
-    {file = "coverage-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620"},
-    {file = "coverage-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d"},
-    {file = "coverage-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536"},
-    {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7"},
-    {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2"},
-    {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4"},
-    {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69"},
-    {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684"},
-    {file = "coverage-6.3.2-cp39-cp39-win32.whl", hash = "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4"},
-    {file = "coverage-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92"},
-    {file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"},
-    {file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"},
+    {file = "coverage-6.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df32ee0f4935a101e4b9a5f07b617d884a531ed5666671ff6ac66d2e8e8246d8"},
+    {file = "coverage-6.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75b5dbffc334e0beb4f6c503fb95e6d422770fd2d1b40a64898ea26d6c02742d"},
+    {file = "coverage-6.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:114944e6061b68a801c5da5427b9173a0dd9d32cd5fcc18a13de90352843737d"},
+    {file = "coverage-6.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ab88a01cd180b5640ccc9c47232e31924d5f9967ab7edd7e5c91c68eee47a69"},
+    {file = "coverage-6.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad8f9068f5972a46d50fe5f32c09d6ee11da69c560fcb1b4c3baea246ca4109b"},
+    {file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4cd696aa712e6cd16898d63cf66139dc70d998f8121ab558f0e1936396dbc579"},
+    {file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c1a9942e282cc9d3ed522cd3e3cab081149b27ea3bda72d6f61f84eaf88c1a63"},
+    {file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c06455121a089252b5943ea682187a4e0a5cf0a3fb980eb8e7ce394b144430a9"},
+    {file = "coverage-6.3.3-cp310-cp310-win32.whl", hash = "sha256:cb5311d6ccbd22578c80028c5e292a7ab9adb91bd62c1982087fad75abe2e63d"},
+    {file = "coverage-6.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:6d4a6f30f611e657495cc81a07ff7aa8cd949144e7667c5d3e680d73ba7a70e4"},
+    {file = "coverage-6.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:79bf405432428e989cad7b8bc60581963238f7645ae8a404f5dce90236cc0293"},
+    {file = "coverage-6.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:338c417613f15596af9eb7a39353b60abec9d8ce1080aedba5ecee6a5d85f8d3"},
+    {file = "coverage-6.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db094a6a4ae6329ed322a8973f83630b12715654c197dd392410400a5bfa1a73"},
+    {file = "coverage-6.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1414e8b124611bf4df8d77215bd32cba6e3425da8ce9c1f1046149615e3a9a31"},
+    {file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:93b16b08f94c92cab88073ffd185070cdcb29f1b98df8b28e6649145b7f2c90d"},
+    {file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fbc86ae8cc129c801e7baaafe3addf3c8d49c9c1597c44bdf2d78139707c3c62"},
+    {file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b5ba058610e8289a07db2a57bce45a1793ec0d3d11db28c047aae2aa1a832572"},
+    {file = "coverage-6.3.3-cp37-cp37m-win32.whl", hash = "sha256:8329635c0781927a2c6ae068461e19674c564e05b86736ab8eb29c420ee7dc20"},
+    {file = "coverage-6.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:e5af1feee71099ae2e3b086ec04f57f9950e1be9ecf6c420696fea7977b84738"},
+    {file = "coverage-6.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e814a4a5a1d95223b08cdb0f4f57029e8eab22ffdbae2f97107aeef28554517e"},
+    {file = "coverage-6.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:61f4fbf3633cb0713437291b8848634ea97f89c7e849c2be17a665611e433f53"},
+    {file = "coverage-6.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3401b0d2ed9f726fadbfa35102e00d1b3547b73772a1de5508ef3bdbcb36afe7"},
+    {file = "coverage-6.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8586b177b4407f988731eb7f41967415b2197f35e2a6ee1a9b9b561f6323c8e9"},
+    {file = "coverage-6.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:892e7fe32191960da559a14536768a62e83e87bbb867e1b9c643e7e0fbce2579"},
+    {file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:afb03f981fadb5aed1ac6e3dd34f0488e1a0875623d557b6fad09b97a942b38a"},
+    {file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cbe91bc84be4e5ef0b1480d15c7b18e29c73bdfa33e07d3725da7d18e1b0aff2"},
+    {file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:91502bf27cbd5c83c95cfea291ef387469f2387508645602e1ca0fd8a4ba7548"},
+    {file = "coverage-6.3.3-cp38-cp38-win32.whl", hash = "sha256:c488db059848702aff30aa1d90ef87928d4e72e4f00717343800546fdbff0a94"},
+    {file = "coverage-6.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6534fcdfb5c503affb6b1130db7b5bfc8a0f77fa34880146f7a5c117987d0"},
+    {file = "coverage-6.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cc692c9ee18f0dd3214843779ba6b275ee4bb9b9a5745ba64265bce911aefd1a"},
+    {file = "coverage-6.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:462105283de203df8de58a68c1bb4ba2a8a164097c2379f664fa81d6baf94b81"},
+    {file = "coverage-6.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc972d829ad5ef4d4c5fcabd2bbe2add84ce8236f64ba1c0c72185da3a273130"},
+    {file = "coverage-6.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:06f54765cdbce99901871d50fe9f41d58213f18e98b170a30ca34f47de7dd5e8"},
+    {file = "coverage-6.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7835f76a081787f0ca62a53504361b3869840a1620049b56d803a8cb3a9eeea3"},
+    {file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6f5fee77ec3384b934797f1873758f796dfb4f167e1296dc00f8b2e023ce6ee9"},
+    {file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:baa8be8aba3dd1e976e68677be68a960a633a6d44c325757aefaa4d66175050f"},
+    {file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4d06380e777dd6b35ee936f333d55b53dc4a8271036ff884c909cf6e94be8b6c"},
+    {file = "coverage-6.3.3-cp39-cp39-win32.whl", hash = "sha256:f8cabc5fd0091976ab7b020f5708335033e422de25e20ddf9416bdce2b7e07d8"},
+    {file = "coverage-6.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c9441d57b0963cf8340268ad62fc83de61f1613034b79c2b1053046af0c5284"},
+    {file = "coverage-6.3.3-pp36.pp37.pp38-none-any.whl", hash = "sha256:d522f1dc49127eab0bfbba4e90fa068ecff0899bbf61bf4065c790ddd6c177fe"},
+    {file = "coverage-6.3.3.tar.gz", hash = "sha256:2781c43bffbbec2b8867376d4d61916f5e9c4cc168232528562a61d1b4b01879"},
 ]
 cssselect = [
     {file = "cssselect-1.1.0-py2.py3-none-any.whl", hash = "sha256:f612ee47b749c877ebae5bb77035d8f4202c6ad0f0fc1271b3c18ad6c4468ecf"},
diff --git a/pylintrc b/pylintrc
index 5ae9a190..b2b66fe6 100644
--- a/pylintrc
+++ b/pylintrc
@@ -26,4 +26,5 @@ disable=
   # annofabcli setting
   no-else-return,
   f-string-without-interpolation,
-  broad-except
\ No newline at end of file
+  broad-except
+  
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index e28796a0..46a659f1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -32,7 +32,7 @@ more-itertools = "*"
 jmespath = "*"
 pyquery = "*"
 isodate = "*"
-annofabapi = ">=0.57.0"
+annofabapi = ">=0.60.2"
 python-datauri = "*"
 pandas = "*"
 bokeh = "^2.4"


Test patch:
diff --git a/pytest.ini b/pytest.ini
index e3243bb9..a43545f8 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -11,6 +11,8 @@ markers =
 # Specify AnnoFab project that has owner role assigned to you.
 project_id = 58a2a621-7d4b-41e7-927b-cdc570c1114a
 task_id = 	testt_24
+# 常に存在する入力データID
+input_data_id = test_id
 
 3dpc_project_id = 3bc1a5e3-73e0-4b2f-b893-dd20f3de5ff9
 3dpc_task_id = 	test5
diff --git a/tests/data/utils/example-utf8.txt b/tests/data/utils/example-utf8.txt
new file mode 100644
index 00000000..456e701b
--- /dev/null
+++ b/tests/data/utils/example-utf8.txt
@@ -0,0 +1,6 @@
+
+a
+
+あ
+
+
diff --git a/tests/data/utils/example-utf8bom.txt b/tests/data/utils/example-utf8bom.txt
new file mode 100644
index 00000000..148840ca
--- /dev/null
+++ b/tests/data/utils/example-utf8bom.txt
@@ -0,0 +1,6 @@
+﻿
+a
+
+あ
+
+
diff --git a/tests/test_annotation.py b/tests/test_annotation.py
index 1f28aae4..4c6471a6 100644
--- a/tests/test_annotation.py
+++ b/tests/test_annotation.py
@@ -1,9 +1,17 @@
+from __future__ import annotations
+
 import configparser
+import datetime
+import json
 import os
+import tempfile
+import time
 from pathlib import Path
 
 import annofabapi
+import more_itertools
 import pytest
+from annofabapi.utils import get_message_for_i18n
 
 from annofabcli.__main__ import main
 
@@ -21,14 +29,127 @@
 annofab_config = dict(inifile.items("annofab"))
 
 project_id = annofab_config["project_id"]
-task_id = annofab_config["task_id"]
+input_data_id = annofab_config["input_data_id"]
 service = annofabapi.build()
 
 
 class TestCommandLine:
+    def _validate_annotation_specs_for_scenario_test(self):
+        """シナリオテストを実行できるアノテーション仕様かどうかを判定する
+
+        Rreturns:
+            Trueならシナリオテストを実行できるアノテーション仕様
+        """
+        annotation_specs, _ = service.api.get_annotation_specs(project_id, query_params={"v": "2"})
+        car_label = more_itertools.first_true(
+            annotation_specs["labels"], pred=lambda e: get_message_for_i18n(e["label_name"]) == "car"
+        )
+        if car_label is None:
+            return False
+
+        if car_label["annotation_type"] != "bounding_box":
+            return False
+
+        truncation_attribute = more_itertools.first_true(
+            annotation_specs["additionals"], pred=lambda e: get_message_for_i18n(e["name"]) == "truncation"
+        )
+        if truncation_attribute is None:
+            return False
+
+        if truncation_attribute["type"] != "flag":
+            return False
+
+        if truncation_attribute["additional_data_definition_id"] not in car_label["additional_data_definitions"]:
+            return False
+
+        return True
+
+    @pytest.fixture
+    def target_task(self):
+        """シナリオテスト用のタスクを作成する
+
+        Yields:
+            作成したタスク
+        """
+        # タスクの作成
+        new_task_id = f"test-{str(datetime.datetime.now().timestamp())}"
+        task, _ = service.api.put_task(project_id, new_task_id, request_body={"input_data_id_list": [input_data_id]})
+        yield task
+        # タスクの削除
+        service.api.delete_task(project_id, new_task_id)
+
+    def test_scenario(self, target_task):
+        """
+        シナリオテスト。すべてのannotation系コマンドを実行する。
+        前提条件：矩形の"car"ラベルに"truncation"チェックボックスが存在すること
+        """
+        assert (
+            self._validate_annotation_specs_for_scenario_test()
+        ), "アノテーション仕様がシナリオテストを実行できる状態でありません。矩形の'car'ラベルを追加して、その下に'turncation'チェックボックスを追加してください。"
+
+        task_id = target_task["task_id"]
+        # インポート用のアノテーションを生成して、アノテーションをインポートする
+        self._execute_import(task_id, input_data_id)
+
+        # すぐに`api.get_annotation_list`を実行すると、インポートしたアノテーションが含まれないので、数秒待つ
+        time.sleep(2)
+
+        # list系のコマンドのテスト
+        self._execute_list(task_id)
+
+        # copyコマンドのテスト
+        self._execute_copy(task_id, input_data_id)
+
+        # 属性とプロパティの変更
+        self._execute_change_properties_and_attributes(task_id, input_data_id)
+
+        # アノテーションのダンプ、削除、リストア
+        self._execute_dump_delete_restore(task_id, input_data_id)
+
     @pytest.mark.depending_on_annotation_specs
-    def test_change_attributes_of_annotation(self):
-        backup_dir = str(out_dir / "backup-annotation")
+    def test_import_3dpc_annotation(self):
+        project_id_of_3dpc = annofab_config["3dpc_project_id"]
+        main(
+            [
+                "annotation",
+                "import",
+                "--project_id",
+                project_id_of_3dpc,
+                "--annotation",
+                str(data_dir / "3dpc-simple-annotation"),
+                "--overwrite",
+                "--yes",
+            ]
+        )
+
+    def write_simple_annotation(self, task_id: str, input_data_id: str, annotation_dir: Path):
+        # インポート用のアノテーションを生成
+        simple_annotation = {
+            "details": [
+                {
+                    "label": "car",
+                    "data": {
+                        "left_top": {"x": 0, "y": 0},
+                        "right_bottom": {"x": 100, "y": 100},
+                        "_type": "BoundingBox",
+                    },
+                    "attributes": {"truncation": True},
+                }
+            ]
+        }
+
+        task_dir = annotation_dir / task_id
+        input_data_json = task_dir / f"{input_data_id}.json"
+
+        task_dir.mkdir(exist_ok=True, parents=True)
+        with input_data_json.open("w", encoding="utf8") as f:
+            json.dump(simple_annotation, f, ensure_ascii=False)
+
+    def _execute_change_properties_and_attributes(self, task_id: str, input_data_id: str):
+        """
+        アノテーションのプロパティの変更、属性の変更のテスト
+        """
+        # truncation属性をFalseに変更する
         main(
             [
                 "annotation",
@@ -37,17 +158,20 @@ def test_change_attributes_of_annotation(self):
                 project_id,
                 "--task_id",
                 task_id,
-                "--backup",
-                backup_dir,
                 "--annotation_query",
-                '{"label_name_en": "car"}',
+                '{"label_name_en":"car"}',
                 "--attributes",
-                '[{"additional_data_definition_name_en": "occluded", "flag": false}]',
+                '[{"additional_data_definition_name_en": "truncation", "flag": false}]',
                 "--yes",
             ]
         )
 
-    def test_change_properties(self):
+        # 属性を確認するためにSimpleアノテーションを取得する
+        simple_annotation, _ = service.api.get_annotation(project_id, task_id, input_data_id)
+        detail = simple_annotation["details"][0]
+        assert detail["attributes"]["truncation"] == False
+
+        # is_protected プロパティを変更する
         main(
             [
                 "annotation",
@@ -62,7 +186,85 @@ def test_change_properties(self):
             ]
         )
 
-    def test_copy_annotation(self):
+        editor_annotation, _ = service.api.get_editor_annotation(project_id, task_id, input_data_id)
+        detail = editor_annotation["details"][0]
+        assert detail["is_protected"] == True
+
+    def _execute_dump_delete_restore(self, task_id: str, input_data_id: str):
+        """
+        アノテーションのダンプ、削除、リストアのテスト
+        """
+        with tempfile.TemporaryDirectory() as str_dump_dir:
+            # アノテーション情報のダンプ
+            main(
+                [
+                    "annotation",
+                    "dump",
+                    "--project_id",
+                    project_id,
+                    "--task_id",
+                    task_id,
+                    "--output_dir",
+                    str_dump_dir,
+                    "--yes",
+                ]
+            )
+
+            # print("すぐに`annotation delete`を実行すると、'ALREADY UPDATED'で失敗する可能性があるので、数秒待つ")
+            time.sleep(2)
+
+            # アノテーションの削除
+            main(["annotation", "delete", "--project_id", project_id, "--task_id", task_id, "--yes"])
+
+            # print("すぐに`annotation delete`を実行すると、'ALREADY UPDATED'で失敗する可能性があるので、数秒待つ")
+            time.sleep(2)
+            editor_annotation, _ = service.api.get_editor_annotation(project_id, task_id, input_data_id)
+            assert len(editor_annotation["details"]) == 0
+
+            # アノテーションのリストア
+            main(
+                [
+                    "annotation",
+                    "restore",
+                    "--project_id",
+                    project_id,
+                    "--task_id",
+                    task_id,
+                    "--annotation",
+                    str_dump_dir,
+                    "--yes",
+                ]
+            )
+            # print("すぐに`annotation delete`を実行すると、'ALREADY UPDATED'で失敗する可能性があるので、数秒待つ")
+            time.sleep(2)
+            editor_annotation, _ = service.api.get_editor_annotation(project_id, task_id, input_data_id)
+            assert len(editor_annotation["details"]) == 1
+
+    def _execute_import(self, task_id: str, input_data_id: str):
+        """
+        アノテーションのインポートのテスト
+        """
+        # インポート用のアノテーションを生成して、アノテーションをインポートする
+        with tempfile.TemporaryDirectory() as str_temp_dir:
+            annotation_dir = Path(str_temp_dir)
+            self.write_simple_annotation(task_id, input_data_id, annotation_dir)
+
+            main(["annotation", "import", "--project_id", project_id, "--annotation", str(annotation_dir), "--yes"])
+
+        simple_annotation, _ = service.api.get_annotation(project_id, task_id, input_data_id)
+        assert len(simple_annotation["details"]) == 1
+        detail = simple_annotation["details"][0]
+        detail["label"] == "car"
+        detail["attributes"]["truncation"] == True
+
+    def _execute_copy(self, src_task_id: str, input_data_id: str):
+        """
+        アノテーションのコピー
+        """
+        dest_task_id = f"{src_task_id}-copy"
+
+        service.api.put_task(project_id, dest_task_id, request_body={"input_data_id_list": [input_data_id]})
+
         main(
             [
                 "annotation",
@@ -70,62 +272,28 @@ def test_copy_annotation(self):
                 "--project_id",
                 project_id,
                 "--input",
-                f"{task_id}:{task_id}",
+                f"{src_task_id}:{dest_task_id}",
                 "--yes",
                 "--overwrite",
-                "--force",
             ]
         )
 
-    def test_dump_annotation(self):
-        output_dir = str(out_dir / "dump-annotation")
-        main(
-            [
-                "annotation",
-                "dump",
-                "--project_id",
-                project_id,
-                "--task_id",
-                task_id,
-                "--output",
-                output_dir,
-                "--yes",
-            ]
-        )
+        dest_editor_annotation, _ = service.api.get_editor_annotation(project_id, dest_task_id, input_data_id)
+        assert len(dest_editor_annotation["details"]) == 1
+        dest_detail = dest_editor_annotation["details"][0]
 
-    def test_import(self):
-        main(
-            [
-                "annotation",
-                "import",
-                "--project_id",
-                project_id,
-                "--annotation",
-                str(data_dir / "imported-annotation.zip"),
-                "--task_id",
-                task_id,
-                "--overwrite",
-                "--yes",
-            ]
-        )
+        src_editor_annotation, _ = service.api.get_editor_annotation(project_id, src_task_id, input_data_id)
+        assert len(src_editor_annotation["details"]) == 1
+        src_detail = src_editor_annotation["details"][0]
 
-    @pytest.mark.depending_on_annotation_specs
-    def test_import_3dpc_annotation(self):
-        project_id_of_3dpc = annofab_config["3dpc_project_id"]
-        main(
-            [
-                "annotation",
-                "import",
-                "--project_id",
-                project_id_of_3dpc,
-                "--annotation",
-                str(data_dir / "3dpc-simple-annotation"),
-                "--overwrite",
-                "--yes",
-            ]
-        )
+        assert src_detail == dest_detail
 
-    def test_list(self):
+        service.api.delete_task(project_id, dest_task_id)
+
+    def _execute_list(self, task_id: str):
+        """
+        list系のコマンドのテスト
+        """
         main(
             [
                 "annotation",
@@ -135,52 +303,19 @@ def test_list(self):
                 "--task_id",
                 task_id,
                 "--output",
-                str(out_dir / "annotation_count.csv"),
+                str(out_dir / "annotation.csv"),
             ]
         )
 
-    def test_list_count(self):
         main(
             [
                 "annotation",
                 "list_count",
                 "--project_id",
                 project_id,
-                "--annotation_query",
-                '{"label_name_en": "car"}',
                 "--task_id",
                 task_id,
                 "--output",
                 str(out_dir / "annotation_count.csv"),
             ]
         )
-
-    def test_delete_and_restore_annotation(self):
-        backup_dir = str(out_dir / "backup-annotation")
-        main(
-            [
-                "annotation",
-                "delete",
-                "--project_id",
-                project_id,
-                "--task_id",
-                task_id,
-                "--backup",
-                backup_dir,
-                "--yes",
-            ]
-        )
-
-        main(
-            [
-                "annotation",
-                "restore",
-                "--project_id",
-                project_id,
-                "--task_id",
-                task_id,
-                "--annotation",
-                backup_dir,
-                "--yes",
-            ]
-        )
diff --git a/tests/test_local_utils.py b/tests/test_local_utils.py
index b39e99ea..895e8927 100644
--- a/tests/test_local_utils.py
+++ b/tests/test_local_utils.py
@@ -1,6 +1,11 @@
 from pathlib import Path
 
-from annofabcli.common.utils import get_file_scheme_path, is_file_scheme, read_multiheader_csv
+from annofabcli.common.utils import (
+    get_file_scheme_path,
+    is_file_scheme,
+    read_lines_except_blank_line,
+    read_multiheader_csv,
+)
 
 out_path = Path("./tests/out/utils")
 data_path = Path("./tests/data/utils")
@@ -20,3 +25,12 @@ def test_get_read_multiheader_csv():
     df = read_multiheader_csv(str(data_path / "multiheader.csv"), header_row_count=2)
     columns = list(df.columns)
     assert columns == [("", "date"), ("task_id", ""), ("worktime", "annotation")]
+
+
+def test_read_lines_except_blank_line():
+    actual = read_lines_except_blank_line(str(data_path / "example-utf8.txt"))
+    assert actual == ["a", "あ"]
+
+    # BOM付きでも読み込めるようにする
+    actual2 = read_lines_except_blank_line(str(data_path / "example-utf8bom.txt"))
+    assert actual2 == ["a", "あ"]