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

Issue:
`task reject`：カスタムプロジェクトの場合、検査コメントを付与できない。
カスタム用の検査コメントを付与できる仕組みが必要。


Discussion / hints:

Fix patch:
diff --git a/.vscode/cspell.json b/.vscode/cspell.json
index 1197829a..c234f9ba 100644
--- a/.vscode/cspell.json
+++ b/.vscode/cspell.json
@@ -21,7 +21,9 @@
         "dateutil",
         "getrgb",
         "inplace",
+        "isin",
         "isna",
+        "isnan",
         "itertools",
         "itertuples",
         "jmespath",
diff --git a/annofabcli/__version__.py b/annofabcli/__version__.py
index e8f5c607..a29c76c7 100644
--- a/annofabcli/__version__.py
+++ b/annofabcli/__version__.py
@@ -1,1 +1,1 @@
-__version__ = "1.45.0"
+__version__ = "1.46.0"
diff --git a/annofabcli/inspection_comment/delete_inspection_comments.py b/annofabcli/inspection_comment/delete_inspection_comments.py
new file mode 100644
index 00000000..9e9d5ac7
--- /dev/null
+++ b/annofabcli/inspection_comment/delete_inspection_comments.py
@@ -0,0 +1,297 @@
+import argparse
+import logging
+import multiprocessing
+import sys
+from typing import Any, Dict, List, Optional, Tuple
+
+import annofabapi
+import annofabapi.utils
+import requests
+from annofabapi.models import ProjectMemberRole, TaskPhase, TaskStatus
+
+import annofabcli
+import annofabcli.common.cli
+from annofabcli import AnnofabApiFacade
+from annofabcli.common.cli import (
+    AbstractCommandLineInterface,
+    AbstractCommandLineWithConfirmInterface,
+    ArgumentParser,
+    build_annofabapi_resource_and_login,
+)
+
+logger = logging.getLogger(__name__)
+
+
+DeletedCommentsForTask = Dict[str, List[str]]
+"""
+タスク配下の削除対象の検査コメント
+keyはinput_data_id, value: input_data_idのlist
+"""
+
+DeletedInspectionIds = Dict[str, DeletedCommentsForTask]
+"""
+削除対象の検査コメント
+keyはtask_id
+"""
+
+
+class DeleteInspectionCommentsMain(AbstractCommandLineWithConfirmInterface):
+    def __init__(self, service: annofabapi.Resource, project_id: str, all_yes: bool = False):
+        self.service = service
+        self.facade = AnnofabApiFacade(service)
+        self.project_id = project_id
+
+        AbstractCommandLineWithConfirmInterface.__init__(self, all_yes)
+
+    def _create_request_body(
+        self, task: Dict[str, Any], input_data_id: str, inspection_ids: List[str]
+    ) -> List[Dict[str, Any]]:
+        """batch_update_inspections に渡すリクエストボディを作成する。"""
+
+        def _convert(inspection_id: str) -> Dict[str, Any]:
+            return {
+                "project_id": self.project_id,
+                "task_id": task["task_id"],
+                "input_data_id": input_data_id,
+                "inspection_id": inspection_id,
+                "_type": "Delete",
+            }
+
+        old_inspection_list, _ = self.service.api.get_inspections(self.project_id, task["task_id"], input_data_id)
+        old_inspection_ids = {e["inspection_id"] for e in old_inspection_list}
+
+        request_body = []
+        for inspection_id in inspection_ids:
+            if inspection_id not in old_inspection_ids:
+                logger.warning(
+                    f"task_id={task['task_id']}, input_data_id={input_data_id}: "
+                    f"inspection_id='{inspection_id}'の検査コメントは存在しません。",
+                )
+                continue
+            request_body.append(_convert(inspection_id))
+        return request_body
+
+    def change_to_working_status(self, project_id: str, task: Dict[str, Any]) -> Dict[str, Any]:
+        """
+        作業中状態に遷移する。必要ならば担当者を自分自身に変更する。
+
+        Args:
+            project_id:
+            task:
+
+        Returns:
+            作業中状態遷移後のタスク
+        """
+
+        task_id = task["task_id"]
+        try:
+            if task["account_id"] != self.service.api.account_id:
+                self.facade.change_operator_of_task(project_id, task_id, self.service.api.account_id)
+                logger.debug(f"{task_id}: 担当者を自分自身に変更しました。")
+
+            changed_task = self.facade.change_to_working_status(project_id, task_id, self.service.api.account_id)
+            return changed_task
+
+        except requests.HTTPError as e:
+            logger.warning(e)
+            logger.warning(f"{task_id}: 担当者の変更、または作業中状態への変更に失敗しました。")
+            raise
+
+    @staticmethod
+    def _can_delete_comment(
+        task: Dict[str, Any],
+    ) -> bool:
+        task_id = task["task_id"]
+        if task["phase"] == TaskPhase.ANNOTATION.value:
+            logger.warning(f"task_id='{task_id}': 教師付フェーズなので、検査コメントを削除できません。")
+            return False
+
+        if task["status"] not in [TaskStatus.NOT_STARTED.value, TaskStatus.WORKING.value, TaskStatus.BREAK.value]:
+            logger.warning(
+                f"task_id='{task_id}' : タスクの状態が未着手,作業中,休憩中 以外の状態なので、検査コメントを削除できません。（task_status='{task['status']}'）"
+            )
+            return False
+        return True
+
+    def delete_comments_for_task(
+        self,
+        task_id: str,
+        inspection_ids_for_task: DeletedCommentsForTask,
+        task_index: Optional[int] = None,
+    ) -> int:
+        """
+        タスクに対して検査コメントを削除します。
+
+        Args:
+            task_id: タスクID
+            inspection_ids_for_task: 削除対象の検査コメントのIDの集合
+            task_index: タスクの連番
+
+        Returns:
+            削除した検査コメントが所属する入力データ数
+        """
+        logging_prefix = f"{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"{logging_prefix} : task_id='{task_id}' のタスクは存在しないので、スキップします。")
+            return 0
+
+        logger.debug(
+            f"{logging_prefix} : task_id = {task['task_id']}, "
+            f"status = {task['status']}, "
+            f"phase = {task['phase']}, "
+        )
+
+        if not self._can_delete_comment(
+            task=task,
+        ):
+            return 0
+
+        if not self.confirm_processing(f"task_id='{task_id}' のタスクに検査コメントを削除しますか？"):
+            return 0
+
+        # 検査コメントを削除するには作業中状態にする必要がある
+        changed_task = self.change_to_working_status(self.project_id, task)
+        added_comments_count = 0
+        for input_data_id, inspection_ids in inspection_ids_for_task.items():
+            if input_data_id not in task["input_data_id_list"]:
+                logger.warning(
+                    f"{logging_prefix} : task_id='{task_id}'のタスクに input_data_id='{input_data_id}'の入力データは存在しません。"
+                )
+                continue
+            try:
+                # 検査コメントを削除
+                request_body = self._create_request_body(
+                    task=changed_task, input_data_id=input_data_id, inspection_ids=inspection_ids
+                )
+                if len(request_body) > 0:
+                    self.service.api.batch_update_inspections(
+                        self.project_id, task_id, input_data_id, request_body=request_body
+                    )
+                    added_comments_count += 1
+                    logger.debug(
+                        f"{logging_prefix} : task_id={task_id}, input_data_id={input_data_id}: "
+                        f"{len(request_body)}件の検査コメントを削除しました。"
+                    )
+                else:
+                    logger.warning(
+                        f"{logging_prefix} : task_id={task_id}, input_data_id={input_data_id}: "
+                        f"削除できる検査コメントは存在しませんでした。"
+                    )
+
+            except Exception as e:  # pylint: disable=broad-except
+                logger.warning(
+                    f"{logging_prefix} : task_id={task_id}, input_data_id={input_data_id}: 検査コメントの削除に失敗しました。", e
+                )
+
+        self.facade.change_to_break_phase(self.project_id, task_id)
+        return added_comments_count
+
+    def delete_comments_for_task_wrapper(
+        self,
+        tpl: Tuple[int, Tuple[str, DeletedCommentsForTask]],
+    ) -> int:
+        task_index, (task_id, inspection_ids_for_task) = tpl
+        return self.delete_comments_for_task(
+            task_id=task_id, inspection_ids_for_task=inspection_ids_for_task, task_index=task_index
+        )
+
+    def delete_comments_for_task_list(
+        self,
+        inspection_ids_for_task_list: DeletedInspectionIds,
+        parallelism: Optional[int] = None,
+    ) -> None:
+        comments_count = sum(len(e) for e in inspection_ids_for_task_list.values())
+        logger.info(f"検査コメントを削除するタスク数: {len(inspection_ids_for_task_list)}, 検査コメントを削除する入力データ数: {comments_count}")
+
+        if parallelism is not None:
+            with multiprocessing.Pool(parallelism) as pool:
+                result_bool_list = pool.map(
+                    self.delete_comments_for_task_wrapper, enumerate(inspection_ids_for_task_list.items())
+                )
+                added_comments_count = sum(e for e in result_bool_list)
+
+        else:
+            # 逐次処理
+            added_comments_count = 0
+            for task_index, (task_id, inspection_ids_for_task) in enumerate(inspection_ids_for_task_list.items()):
+                try:
+                    result = self.delete_comments_for_task(
+                        task_id=task_id,
+                        inspection_ids_for_task=inspection_ids_for_task,
+                        task_index=task_index,
+                    )
+                    added_comments_count += result
+                except Exception as e:  # pylint: disable=broad-except
+                    logger.warning(f"task_id={task_id}: 検査コメントの削除に失敗しました。", e)
+                    continue
+
+        logger.info(f"{added_comments_count} / {comments_count} 件の入力データから検査コメントを削除しました。")
+
+
+class DeleteInspectionComments(AbstractCommandLineInterface):
+    @staticmethod
+    def validate(args: argparse.Namespace) -> bool:
+        COMMON_MESSAGE = "annofabcli inspection_comment delete: error:"
+
+        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):
+        args = self.args
+        if not self.validate(args):
+            return
+
+        super().validate_project(args.project_id, [ProjectMemberRole.ACCEPTER, ProjectMemberRole.OWNER])
+
+        dict_comments = annofabcli.common.cli.get_json_from_args(args.json)
+        main_obj = DeleteInspectionCommentsMain(self.service, project_id=args.project_id, all_yes=self.all_yes)
+        main_obj.delete_comments_for_task_list(
+            inspection_ids_for_task_list=dict_comments,
+            parallelism=args.parallelism,
+        )
+
+
+def main(args: argparse.Namespace):
+    service = build_annofabapi_resource_and_login(args)
+    facade = AnnofabApiFacade(service)
+    DeleteInspectionComments(service, facade, args).main()
+
+
+def parse_args(parser: argparse.ArgumentParser):
+    argument_parser = ArgumentParser(parser)
+
+    argument_parser.add_project_id()
+
+    parser.add_argument(
+        "--json",
+        type=str,
+        help=(
+            "削除する検査コメントのIDをJSON形式で指定してください。"
+            "JSONのスキーマは https://annofab-cli.readthedocs.io/ja/latest/command_reference/inspection_comment/delete.html "
+            "に記載されています。"
+        ),
+    )
+
+    parser.add_argument(
+        "--parallelism", type=int, help="使用するプロセス数（並列度）を指定してください。指定する場合は必ず'--yes'を指定してください。指定しない場合は、逐次的に処理します。"
+    )
+
+    parser.set_defaults(subcommand_func=main)
+
+
+def add_parser(subparsers: argparse._SubParsersAction):
+    subcommand_name = "delete"
+    subcommand_help = "検査コメントを削除します。"
+    description = "検査コメントを削除します。 【注意】他人の検査コメントや他のフェーズで付与された検査コメントを削除できてしまいます。"
+    epilog = "チェッカーロールまたはオーナロールを持つユーザで実行してください。"
+
+    parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog)
+    parse_args(parser)
diff --git a/annofabcli/inspection_comment/put_inspection_comments.py b/annofabcli/inspection_comment/put_inspection_comments.py
new file mode 100644
index 00000000..b5e363c3
--- /dev/null
+++ b/annofabcli/inspection_comment/put_inspection_comments.py
@@ -0,0 +1,332 @@
+import argparse
+import logging
+import multiprocessing
+import sys
+import uuid
+from dataclasses import dataclass
+from typing import Any, Dict, List, Optional, Tuple
+
+import annofabapi
+import annofabapi.utils
+import requests
+from annofabapi.models import ProjectMemberRole, TaskPhase, TaskStatus
+from dataclasses_json import DataClassJsonMixin
+
+import annofabcli
+import annofabcli.common.cli
+from annofabcli import AnnofabApiFacade
+from annofabcli.common.cli import (
+    AbstractCommandLineInterface,
+    AbstractCommandLineWithConfirmInterface,
+    ArgumentParser,
+    build_annofabapi_resource_and_login,
+)
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class AddedComment(DataClassJsonMixin):
+    """
+    追加対象の検査コメント
+    """
+
+    comment: str
+    """検査コメントの中身"""
+
+    data: Dict[str, Any]
+    """検査コメントを付与する位置や区間"""
+
+    annotation_id: Optional[str] = None
+    """検査コメントに紐付ける"""
+
+    phrases: Optional[List[str]] = None
+    """参照している定型指摘ID"""
+
+
+AddedCommentsForTask = Dict[str, List[AddedComment]]
+"""
+タスク配下の追加対象の検査コメント
+keyはinput_data_id
+"""
+
+AddedComments = Dict[str, AddedCommentsForTask]
+"""
+追加対象の検査コメント
+keyはtask_id
+"""
+
+
+class AddInspectionCommentsMain(AbstractCommandLineWithConfirmInterface):
+    def __init__(self, service: annofabapi.Resource, project_id: str, all_yes: bool = False):
+        self.service = service
+        self.facade = AnnofabApiFacade(service)
+        self.project_id = project_id
+
+        AbstractCommandLineWithConfirmInterface.__init__(self, all_yes)
+
+    def _create_request_body(
+        self, task: Dict[str, Any], input_data_id: str, comments: List[AddedComment]
+    ) -> List[Dict[str, Any]]:
+        """batch_update_inspections に渡すリクエストボディを作成する。"""
+
+        def _create_dict_annotation_id() -> Dict[str, str]:
+            content, _ = self.service.api.get_editor_annotation(self.project_id, task["task_id"], input_data_id)
+            details = content["details"]
+            return {e["annotation_id"]: e["label_id"] for e in details}
+
+        dict_annotation_id_label_id = _create_dict_annotation_id()
+
+        def _convert(comment: AddedComment) -> Dict[str, Any]:
+            return {
+                "data": {
+                    "project_id": self.project_id,
+                    "comment": comment.comment,
+                    "task_id": task["task_id"],
+                    "input_data_id": input_data_id,
+                    "inspection_id": str(uuid.uuid4()),
+                    "phase": task["phase"],
+                    "commenter_account_id": self.service.api.account_id,
+                    "data": comment.data,
+                    "annotation_id": comment.annotation_id,
+                    "phrases": comment.phrases,
+                    "status": "annotator_action_required",
+                    "created_datetime": task["updated_datetime"],
+                    "label_id": dict_annotation_id_label_id.get(comment.annotation_id)
+                    if comment.annotation_id is not None
+                    else None,
+                },
+                "_type": "Put",
+            }
+
+        return [_convert(e) for e in comments]
+
+    def change_to_working_status(self, project_id: str, task: Dict[str, Any]) -> Dict[str, Any]:
+        """
+        作業中状態に遷移する。必要ならば担当者を自分自身に変更する。
+
+        Args:
+            project_id:
+            task:
+
+        Returns:
+            作業中状態遷移後のタスク
+        """
+
+        task_id = task["task_id"]
+        try:
+            if task["account_id"] != self.service.api.account_id:
+                self.facade.change_operator_of_task(project_id, task_id, self.service.api.account_id)
+                logger.debug(f"{task_id}: 担当者を自分自身に変更しました。")
+
+            changed_task = self.facade.change_to_working_status(project_id, task_id, self.service.api.account_id)
+            return changed_task
+
+        except requests.HTTPError as e:
+            logger.warning(e)
+            logger.warning(f"{task_id}: 担当者の変更、または作業中状態への変更に失敗しました。")
+            raise
+
+    @staticmethod
+    def _can_add_comment(
+        task: Dict[str, Any],
+    ) -> bool:
+        task_id = task["task_id"]
+        if task["phase"] == TaskPhase.ANNOTATION.value:
+            logger.warning(f"task_id='{task_id}': 教師付フェーズなので、検査コメントを付与できません。")
+            return False
+
+        if task["status"] not in [TaskStatus.NOT_STARTED.value, TaskStatus.WORKING.value, TaskStatus.BREAK.value]:
+            logger.warning(
+                f"task_id='{task_id}' : タスクの状態が未着手,作業中,休憩中 以外の状態なので、検査コメントを付与できません。（task_status='{task['status']}'）"
+            )
+            return False
+        return True
+
+    def add_comments_for_task(
+        self,
+        task_id: str,
+        comments_for_task: AddedCommentsForTask,
+        task_index: Optional[int] = None,
+    ) -> int:
+        """
+        タスクに検査コメントを付与します。
+
+        Args:
+            task_id: タスクID
+            comments_for_task: 1つのタスクに付与する検査コメントの集合
+            task_index: タスクの連番
+
+        Returns:
+            付与した検査コメントの数
+        """
+        logging_prefix = f"{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"{logging_prefix} : task_id='{task_id}' のタスクは存在しないので、スキップします。")
+            return 0
+
+        logger.debug(
+            f"{logging_prefix} : task_id = {task['task_id']}, "
+            f"status = {task['status']}, "
+            f"phase = {task['phase']}, "
+        )
+
+        if not self._can_add_comment(
+            task=task,
+        ):
+            return 0
+
+        if not self.confirm_processing(f"task_id='{task_id}' のタスクに検査コメントを付与しますか？"):
+            return 0
+
+        # 検査コメントを付与するには作業中状態にする必要がある
+        changed_task = self.change_to_working_status(self.project_id, task)
+        added_comments_count = 0
+        for input_data_id, comments in comments_for_task.items():
+            if input_data_id not in task["input_data_id_list"]:
+                logger.warning(
+                    f"{logging_prefix} : task_id='{task_id}'のタスクに input_data_id='{input_data_id}'の入力データは存在しません。"
+                )
+                continue
+            try:
+                # 検査コメントを付与する
+                request_body = self._create_request_body(
+                    task=changed_task, input_data_id=input_data_id, comments=comments
+                )
+                self.service.api.batch_update_inspections(
+                    self.project_id, task_id, input_data_id, request_body=request_body
+                )
+                added_comments_count += 1
+                logger.debug(
+                    f"{logging_prefix} : task_id={task_id}, input_data_id={input_data_id}: "
+                    f"{len(comments)}件の検査コメントを付与しました。"
+                )
+            except Exception as e:  # pylint: disable=broad-except
+                logger.warning(
+                    f"{logging_prefix} : task_id={task_id}, input_data_id={input_data_id}: 検査コメントの付与に失敗しました。", e
+                )
+
+        self.facade.change_to_break_phase(self.project_id, task_id)
+        return added_comments_count
+
+    def add_comments_for_task_wrapper(
+        self,
+        tpl: Tuple[int, Tuple[str, AddedCommentsForTask]],
+    ) -> int:
+        task_index, (task_id, comments_for_task) = tpl
+        return self.add_comments_for_task(task_id=task_id, comments_for_task=comments_for_task, task_index=task_index)
+
+    def add_comments_for_task_list(
+        self,
+        comments_for_task_list: AddedComments,
+        parallelism: Optional[int] = None,
+    ) -> None:
+        comments_count = sum(len(e) for e in comments_for_task_list.values())
+        logger.info(f"検査コメントを付与するタスク数: {len(comments_for_task_list)}, 検査コメント付与する入力データ数: {comments_count}")
+
+        if parallelism is not None:
+            with multiprocessing.Pool(parallelism) as pool:
+                result_bool_list = pool.map(
+                    self.add_comments_for_task_wrapper, enumerate(comments_for_task_list.items())
+                )
+                added_comments_count = sum(e for e in result_bool_list)
+
+        else:
+            # 逐次処理
+            added_comments_count = 0
+            for task_index, (task_id, comments_for_task) in enumerate(comments_for_task_list.items()):
+                try:
+                    result = self.add_comments_for_task(
+                        task_id=task_id,
+                        comments_for_task=comments_for_task,
+                        task_index=task_index,
+                    )
+                    added_comments_count += result
+                except Exception as e:  # pylint: disable=broad-except
+                    logger.warning(f"task_id={task_id}: 検査コメントの付与に失敗しました。", e)
+                    continue
+
+        logger.info(f"{added_comments_count} / {comments_count} 件の入力データに検査コメントを付与しました。")
+
+
+class PutInspectionComments(AbstractCommandLineInterface):
+    @staticmethod
+    def validate(args: argparse.Namespace) -> bool:
+        COMMON_MESSAGE = "annofabcli inspection_comment put: error:"
+
+        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
+
+    @staticmethod
+    def _convert_comments(dict_comments: Dict[str, Any]) -> AddedComments:
+        def _covert_comments_for_task(comments_for_task):
+            return {
+                input_data_id: AddedComment.schema().load(comments, many=True)
+                for input_data_id, comments in comments_for_task.items()
+            }
+
+        return {
+            task_id: _covert_comments_for_task(comments_for_task)
+            for task_id, comments_for_task in dict_comments.items()
+        }
+
+    def main(self):
+        args = self.args
+        if not self.validate(args):
+            return
+
+        super().validate_project(args.project_id, [ProjectMemberRole.ACCEPTER, ProjectMemberRole.OWNER])
+
+        dict_comments = annofabcli.common.cli.get_json_from_args(args.json)
+        comments_for_task_list = self._convert_comments(dict_comments)
+        main_obj = AddInspectionCommentsMain(self.service, project_id=args.project_id, all_yes=self.all_yes)
+        main_obj.add_comments_for_task_list(
+            comments_for_task_list=comments_for_task_list,
+            parallelism=args.parallelism,
+        )
+
+
+def main(args: argparse.Namespace):
+    service = build_annofabapi_resource_and_login(args)
+    facade = AnnofabApiFacade(service)
+    PutInspectionComments(service, facade, args).main()
+
+
+def parse_args(parser: argparse.ArgumentParser):
+    argument_parser = ArgumentParser(parser)
+
+    argument_parser.add_project_id()
+
+    parser.add_argument(
+        "--json",
+        type=str,
+        help=(
+            "付与する検査コメントをJSON形式で指定してください。"
+            "JSONのスキーマは https://annofab-cli.readthedocs.io/ja/latest/command_reference/inspection_comment/put.html "
+            "に記載されています。"
+        ),
+    )
+
+    parser.add_argument(
+        "--parallelism", type=int, help="使用するプロセス数（並列度）を指定してください。指定する場合は必ず'--yes'を指定してください。指定しない場合は、逐次的に処理します。"
+    )
+
+    parser.set_defaults(subcommand_func=main)
+
+
+def add_parser(subparsers: argparse._SubParsersAction):
+    subcommand_name = "put"
+    subcommand_help = "検査コメントを付与します。"
+    description = "検査コメントを付与します。"
+    epilog = "チェッカーロールまたはオーナロールを持つユーザで実行してください。"
+
+    parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog)
+    parse_args(parser)
diff --git a/annofabcli/inspection_comment/subcommand_inspection_comment.py b/annofabcli/inspection_comment/subcommand_inspection_comment.py
index cfcccc5f..b0d527f8 100644
--- a/annofabcli/inspection_comment/subcommand_inspection_comment.py
+++ b/annofabcli/inspection_comment/subcommand_inspection_comment.py
@@ -2,8 +2,9 @@
 
 import annofabcli
 import annofabcli.common.cli
-import annofabcli.inspection_comment.list_inspections
+import annofabcli.inspection_comment.delete_inspection_comments
 import annofabcli.inspection_comment.list_inspections_with_json
+import annofabcli.inspection_comment.put_inspection_comments
 
 
 def parse_args(parser: argparse.ArgumentParser):
@@ -11,8 +12,10 @@ def parse_args(parser: argparse.ArgumentParser):
     subparsers = parser.add_subparsers(dest="subcommand_name")
 
     # サブコマンドの定義
+    annofabcli.inspection_comment.delete_inspection_comments.add_parser(subparsers)
     annofabcli.inspection_comment.list_inspections.add_parser(subparsers)
     annofabcli.inspection_comment.list_inspections_with_json.add_parser(subparsers)
+    annofabcli.inspection_comment.put_inspection_comments.add_parser(subparsers)
 
 
 def add_parser(subparsers: argparse._SubParsersAction):
diff --git a/annofabcli/task/reject_tasks.py b/annofabcli/task/reject_tasks.py
index 984af721..32d4aa98 100644
--- a/annofabcli/task/reject_tasks.py
+++ b/annofabcli/task/reject_tasks.py
@@ -27,26 +27,23 @@
 
 
 class RejectTasksMain(AbstractCommandLineWithConfirmInterface):
-    def __init__(self, service: annofabapi.Resource, all_yes: bool = False):
+    def __init__(self, service: annofabapi.Resource, *, comment_data: Optional[Dict[str, Any]], all_yes: bool = False):
         self.service = service
         self.facade = AnnofabApiFacade(service)
+        self.comment_data = comment_data
         AbstractCommandLineWithConfirmInterface.__init__(self, all_yes)
 
     def add_inspection_comment(
         self,
         project_id: str,
-        project_input_data_type: InputDataType,
         task: Dict[str, Any],
         inspection_comment: str,
     ):
         """
         検査コメントを付与する。
-        画像プロジェクトなら先頭画像の左上に付与する。
-        動画プロジェクトなら最初の区間に付与する。
 
         Args:
             project_id:
-            project_input_data_type: プロジェクトの入力データの種類
             task:
             inspection_comment:
 
@@ -55,10 +52,6 @@ def add_inspection_comment(
 
         """
         first_input_data_id = task["input_data_id_list"][0]
-        if project_input_data_type == InputDataType.MOVIE:
-            inspection_data = {"start": 0, "end": 100, "_type": "Time"}
-        else:
-            inspection_data = {"x": 0, "y": 0, "_type": "Point"}
 
         req_inspection = [
             {
@@ -70,7 +63,7 @@ def add_inspection_comment(
                     "inspection_id": str(uuid.uuid4()),
                     "phase": task["phase"],
                     "commenter_account_id": self.service.api.account_id,
-                    "data": inspection_data,
+                    "data": self.comment_data,
                     "status": "annotator_action_required",
                     "created_datetime": task["updated_datetime"],
                 },
@@ -169,7 +162,6 @@ def reject_task_with_adding_comment(
         self,
         project_id: str,
         task_id: str,
-        project_input_data_type: InputDataType,
         inspection_comment: Optional[str] = None,
         assign_last_annotator: bool = True,
         assigned_annotator_user_id: Optional[str] = None,
@@ -220,7 +212,7 @@ def reject_task_with_adding_comment(
             changed_task = self.change_to_working_status(project_id, task)
             try:
                 # 検査コメントを付与する
-                self.add_inspection_comment(project_id, project_input_data_type, changed_task, inspection_comment)
+                self.add_inspection_comment(project_id, changed_task, inspection_comment)
                 logger.debug(f"{logging_prefix} : task_id = {task_id}, 検査コメントの付与 完了")
 
             except requests.exceptions.HTTPError:
@@ -261,7 +253,6 @@ def reject_task_for_task_wrapper(
         self,
         tpl: Tuple[int, str],
         project_id: str,
-        project_input_data_type: InputDataType,
         inspection_comment: Optional[str] = None,
         assign_last_annotator: bool = True,
         assigned_annotator_user_id: Optional[str] = None,
@@ -273,7 +264,6 @@ def reject_task_for_task_wrapper(
             project_id=project_id,
             task_id=task_id,
             task_index=task_index,
-            project_input_data_type=project_input_data_type,
             inspection_comment=inspection_comment,
             assign_last_annotator=assign_last_annotator,
             assigned_annotator_user_id=assigned_annotator_user_id,
@@ -295,16 +285,12 @@ def reject_task_list(
         if task_query is not None:
             task_query = self.facade.set_account_id_of_task_query(project_id, task_query)
 
-        project, _ = self.service.api.get_project(project_id)
-        project_input_data_type = InputDataType(project["input_data_type"])
-
         logger.info(f"差し戻すタスク数: {len(task_id_list)}")
 
         if parallelism is not None:
             partial_func = partial(
                 self.reject_task_for_task_wrapper,
                 project_id=project_id,
-                project_input_data_type=project_input_data_type,
                 inspection_comment=inspection_comment,
                 assign_last_annotator=assign_last_annotator,
                 assigned_annotator_user_id=assigned_annotator_user_id,
@@ -323,7 +309,6 @@ def reject_task_list(
                     project_id,
                     task_id,
                     task_index=task_index,
-                    project_input_data_type=project_input_data_type,
                     inspection_comment=inspection_comment,
                     assign_last_annotator=assign_last_annotator,
                     assigned_annotator_user_id=assigned_annotator_user_id,
@@ -337,13 +322,13 @@ def reject_task_list(
 
 
 class RejectTasks(AbstractCommandLineInterface):
-    @staticmethod
-    def validate(args: argparse.Namespace) -> bool:
-        COMMON_MESSAGE = "annofabcli task reject: error:"
+    COMMON_MESSAGE = "annofabcli task reject: error:"
+
+    def validate(self, args: argparse.Namespace) -> bool:
 
         if args.parallelism is not None and not args.yes:
             print(
-                f"{COMMON_MESSAGE} argument --parallelism: '--parallelism'を指定するときは、必ず'--yes'を指定してください。",
+                f"{self.COMMON_MESSAGE} argument --parallelism: '--parallelism'を指定するときは、必ず'--yes'を指定してください。",
                 file=sys.stderr,
             )
             return False
@@ -363,7 +348,23 @@ def main(self):
         dict_task_query = annofabcli.common.cli.get_json_from_args(args.task_query)
         task_query: Optional[TaskQuery] = TaskQuery.from_dict(dict_task_query) if dict_task_query is not None else None
 
-        main_obj = RejectTasksMain(self.service, all_yes=self.all_yes)
+        comment_data = annofabcli.common.cli.get_json_from_args(args.comment_data)
+        project, _ = self.service.api.get_project(args.project_id)
+        if args.comment is not None and comment_data is None:
+            if project["input_data_type"] == InputDataType.IMAGE.value:
+                comment_data = {"x": 0, "y": 0, "_type": "Point"}
+            elif project["input_data_type"] == InputDataType.MOVIE.value:
+                # 注意：少なくとも0.1秒以上の区間にしないと、Annofab上で検査コメントを確認できない
+                comment_data = {"start": 0, "end": 100, "_type": "Time"}
+            elif project["input_data_type"] == InputDataType.CUSTOM.value:
+                # customプロジェクト
+                print(
+                    f"{self.COMMON_MESSAGE} argument --comment_data: カスタムプロジェクトに検査コメントを付与する場合は必須です。",
+                    file=sys.stderr,
+                )
+                return
+
+        main_obj = RejectTasksMain(self.service, comment_data=comment_data, all_yes=self.all_yes)
         main_obj.reject_task_list(
             args.project_id,
             task_id_list,
@@ -392,7 +393,17 @@ def parse_args(parser: argparse.ArgumentParser):
         "-c",
         "--comment",
         type=str,
-        help="差し戻すときに付与する検査コメントを指定します。" "画像プロジェクトならばタスク内の先頭の画像の左上(x=0,y=0)に、動画プロジェクトなら動画の先頭（start=0, end=100)に付与します。",
+        help="差し戻すときに付与する検査コメントを指定します。検査コメントはタスク内の先頭画像に付与します。付与する位置は'--comment_data'で指定できます。",
+    )
+
+    parser.add_argument(
+        "--comment_data",
+        type=str,
+        help="検査コメントを付与する位置や区間をJSON形式で指定します。"
+        "指定方法は https://annofab-cli.readthedocs.io/ja/latest/command_reference/task/reject.html を参照してください。"
+        "`file://`を先頭に付けると、JSON形式のファイルを指定できます。"
+        "デフォルトでは画像プロジェクトならば画像の左上(x=0,y=0)、動画プロジェクトなら動画の先頭（start=0, end=100)に付与します。"
+        "カスタムプロジェクトに検査コメントを付与する場合は必須です。",
     )
 
     # 差し戻したタスクの担当者の割当に関して
diff --git a/docs/command_reference/inspection_comment/delete.rst b/docs/command_reference/inspection_comment/delete.rst
new file mode 100644
index 00000000..3cf13da7
--- /dev/null
+++ b/docs/command_reference/inspection_comment/delete.rst
@@ -0,0 +1,51 @@
+==========================================
+inspection_comment delete
+==========================================
+
+Description
+=================================
+検査コメントを削除します。
+
+
+
+Examples
+=================================
+
+基本的な使い方
+--------------------------
+
+``--json`` に削除する検査コメントのinspection_idをJSON形式で指定してください。
+
+.. code-block::
+    :caption: inspection_id.json
+
+    {
+        "task1":{
+            "input_data1": ["inspection1","inspection2"],
+            "input_data2":[{},{}, ...]
+        },
+        "task2": {
+            "input_data3":[{},{}, ...]
+        }
+    }
+
+
+
+.. code-block::
+
+    $ annofabcli inspection_comment delete --project_id prj1 --json file://inspection_id.json
+
+
+並列処理
+----------------------------------------------
+
+以下のコマンドは、並列数4で実行します。
+
+.. code-block::
+
+    $  annofabcli inspection_comment delete --project_id prj1 --json file://inspection_id.json \
+    --parallelism 4 --yes
+
+
+    
+    
\ No newline at end of file
diff --git a/docs/command_reference/inspection_comment/index.rst b/docs/command_reference/inspection_comment/index.rst
index cb7088f5..9db4e50d 100644
--- a/docs/command_reference/inspection_comment/index.rst
+++ b/docs/command_reference/inspection_comment/index.rst
@@ -15,5 +15,7 @@ Available Commands
    :maxdepth: 1
    :titlesonly:
 
+   delete
    list
-   list_with_json
\ No newline at end of file
+   list_with_json
+   put
\ No newline at end of file
diff --git a/docs/command_reference/inspection_comment/put.rst b/docs/command_reference/inspection_comment/put.rst
new file mode 100644
index 00000000..3fede16f
--- /dev/null
+++ b/docs/command_reference/inspection_comment/put.rst
@@ -0,0 +1,81 @@
+==========================================
+inspection_comment put
+==========================================
+
+Description
+=================================
+検査コメントを付与します。
+
+
+
+Examples
+=================================
+
+基本的な使い方
+--------------------------
+
+``--json`` に検査コメントの内容をJSON形式で指定してください。
+
+.. code-block::
+    :caption: comment.json
+
+    {
+        "task1":{
+            "input_data1": [
+                {
+                    "comment": "type属性が間違っています。",
+                    "data": { // 点で指摘
+                        "x":10,
+                        "y":20,
+                        "_type": "Point"
+                    }
+                },
+                {
+                    "comment": "枠がズレています。 #A1",
+                    "data": { // ポリラインで指摘
+                        "coordinates":[
+                            {"x":20, "y":20}, {"x":30, "y":30}
+                        ],
+                        "_type": "Polyline"
+                    },
+                    "annotation_id": "foo", //アノテーションに紐づける
+                    "phrases": ["A1"] //定型指摘コメントを利用する
+                }
+            ],
+            "input_data2":[{},{}, ...]
+        },
+        "task2": {
+            "input_data3":[{},{}, ...]
+        }
+    }
+
+
+* 1階層目のキーはtask_id, 2階層目のキーはinput_data_idです。
+* 検査コメントのプロパティとして指定できるキーは以下の通りです。
+
+  * ``comment``：検査コメントの内容。必須。
+  * ``data``：検査コメントの位置や区間。必須。詳細は https://annofab.com/docs/api/#operation/batchUpdateInspections のリクエストボディを参照してください。
+  * ``annotation_id``：検査コメントに紐づくアノテーションのannotation_id
+  * ``phrases``：参照する定型指摘のIDの配列
+
+
+
+
+.. code-block::
+
+    $ annofabcli inspection_comment put --project_id prj1 --json file://comment.json
+
+
+並列処理
+----------------------------------------------
+
+以下のコマンドは、並列数4で実行します。
+
+.. code-block::
+
+    $  annofabcli inspection_comment put --project_id prj1 --json file://comment.json \
+    --parallelism 4 --yes
+
+
+    
+    
\ No newline at end of file
diff --git a/docs/command_reference/task/reject.rst b/docs/command_reference/task/reject.rst
index 42c8aaf6..8f937943 100644
--- a/docs/command_reference/task/reject.rst
+++ b/docs/command_reference/task/reject.rst
@@ -46,10 +46,7 @@ Examples
 
 
 差し戻す際に検査コメントを付与する場合は、``--comment`` を指定してください。
-検査コメントの付与する場所は、以下の通りです。
-
-* 画像プロジェクト: 1番目の画像の左上( ``x=0, y=0`` )
-* 動画プロジェクト: 動画の先頭( ``start=0, end=100`` )
+検査コメントの付与する場所は、``--comment_data`` で指定できます。後述を参照してください。
 
 
 .. code-block::
@@ -57,6 +54,49 @@ Examples
     $ annofabcli task reject --project_id prj1 --task_id file://task_id.txt \
      --comment "weather属性を見直してください。"
 
+検査コメントの位置や区間を指定する
+--------------------------------------
+``--comment_data`` に、検査コメントの位置や区間をJSON形式で指定することができます。
+
+.. code-block::
+
+    $ annofabcli task reject --project_id prj1 --task_id file://task_id.txt \
+     --comment "weather属性を見直してください。" \
+     --comment_data '{"x":10,"y":10,"_type":"Point"}'
+
+``--comment_data`` に渡す形式は、https://annofab.com/docs/api/#operation/batchUpdateInspections APIのリクエストボディ ``data`` を参照してください。
+
+以下、サンプルです。
+
+.. code-block::
+
+    // 画像プロジェクト：(x=0,y=0)の位置に点
+    {
+        "x":0,
+        "y":0,
+        "_type": "Point"
+    }
+
+    // 動画プロジェクト：0〜100ミリ秒の区間
+    {
+        "start":0,
+        "end":100,
+        "_type": "Time"
+    }
+
+    // カスタムプロジェクト（3dpc editor）：原点付近に辺が1の立方体
+    {
+        "data": "{\"kind\": \"CUBOID\", \"shape\": {\"dimensions\": {\"width\": 1.0, \"height\": 1.0, \"depth\": 1.0}, \"location\": {\"x\": 1.0, \"y\": 1.0, \"z\": 1.0}, \"rotation\": {\"x\": -0.020482505352460902, \"y\": -0.013537527746278588, \"z\": -1.92700223982741}, \"direction\": {\"front\": {\"x\": 1.0, \"y\": 0.0, \"z\": 0.0}, \"up\": {\"x\": 0.0, \"y\": 0.0, \"z\": 1.0}}}, \"version\": \"2\"}",
+        "_type": "Custom"    
+    }
+
+
+``--comment_data`` を指定しない場合は、以下の値になります。
+
+* 画像プロジェクト： ``{"x":0, "y":0, "_type": "Point"}``
+* 動画プロジェクト： ``{"start":0, "end":100, "_type": "Time"}``
+
+カスタムプロジェクトの場合は、検査コメントの位置を決められないので、 ``--comment_data`` は必須です。
 
 
 差し戻したタスクの担当者を指定する
diff --git a/poetry.lock b/poetry.lock
index 1fdf57e3..1554626f 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -38,7 +38,7 @@ python-versions = "*"
 
 [[package]]
 name = "astroid"
-version = "2.6.5"
+version = "2.6.6"
 description = "An abstract syntax tree for Python with inference support."
 category = "dev"
 optional = false
@@ -151,11 +151,11 @@ uvloop = ["uvloop (>=0.15.2)"]
 
 [[package]]
 name = "bleach"
-version = "3.3.1"
+version = "4.0.0"
 description = "An easy safelist-based HTML-sanitizing tool."
 category = "main"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+python-versions = ">=3.6"
 
 [package.dependencies]
 packaging = "*"
@@ -190,7 +190,7 @@ python-versions = "*"
 
 [[package]]
 name = "charset-normalizer"
-version = "2.0.3"
+version = "2.0.4"
 description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
 category = "main"
 optional = false
@@ -377,7 +377,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
 [[package]]
 name = "importlib-metadata"
-version = "4.6.1"
+version = "4.6.4"
 description = "Read metadata from Python packages"
 category = "main"
 optional = false
@@ -402,7 +402,7 @@ python-versions = "*"
 
 [[package]]
 name = "ipython"
-version = "7.25.0"
+version = "7.26.0"
 description = "IPython: Productive Interactive Computing"
 category = "dev"
 optional = false
@@ -661,7 +661,7 @@ test = ["pytest (>=5.0.1)", "pytest-xdist", "hypothesis (>=3.58)"]
 
 [[package]]
 name = "panel"
-version = "0.12.0"
+version = "0.12.1"
 description = "A high level app and dashboarding solution for Python."
 category = "main"
 optional = false
@@ -675,10 +675,10 @@ param = ">=1.10.0"
 pyct = ">=0.4.4"
 pyviz-comms = ">=0.7.4"
 requests = "*"
-tqdm = "*"
+tqdm = ">=4.48.0"
 
 [package.extras]
-all = ["altair", "channels", "codecov", "datashader", "django", "flake8", "folium", "graphviz", "holoviews", "holoviews (>1.14.1)", "hvplot", "ipyleaflet", "ipympl", "ipython (>=7.0)", "ipyvolume", "ipywidgets", "ipywidgets-bokeh", "jupyter-bokeh (>=3.0.2)", "lxml", "matplotlib (<3.4)", "nbsite (>=0.7.0a4)", "nbsmoke (>=0.2.0)", "notebook (>=5.4)", "pandas (>=1.3)", "parameterized", "pillow", "plotly", "plotly (>=4.0)", "pytest", "pytest-cov", "pyvista", "scikit-learn", "scipy", "streamz", "twine", "vega-datasets", "vtk (==9.0.1)", "xarray (<0.17)"]
+all = ["notebook (>=5.4)", "altair", "channels", "codecov", "datashader", "django", "flake8", "folium", "graphviz", "holoviews", "holoviews (>1.14.1)", "hvplot", "ipyleaflet", "ipympl", "ipython (>=7.0)", "ipyvolume", "ipywidgets", "ipywidgets-bokeh", "jupyter-bokeh (>=3.0.2)", "lxml", "matplotlib (<3.4)", "nbsite (>=0.7.0a4)", "nbsmoke (>=0.2.0)", "pandas (>=1.3)", "parameterized", "pillow", "plotly", "plotly (>=4.0)", "pytest", "pytest-cov", "pyvista", "scikit-learn", "scipy", "streamz", "twine", "vega-datasets", "vtk (==9.0.1)", "xarray (<0.17)"]
 build = ["param (>=1.9.2)", "pyct (>=0.4.4)", "setuptools (>=30.3.0)", "bokeh (>=2.0.0)", "pyviz-comms (>=0.6.0)", "bleach", "tqdm"]
 doc = ["notebook (>=5.4)", "holoviews (>1.14.1)", "matplotlib (<3.4)", "pillow", "plotly", "nbsite (>=0.7.0a4)", "graphviz", "lxml"]
 examples = ["hvplot", "plotly (>=4.0)", "altair", "streamz", "vega-datasets", "vtk (==9.0.1)", "scikit-learn", "datashader", "jupyter-bokeh (>=3.0.2)", "django", "channels", "pyvista", "ipywidgets", "ipywidgets-bokeh", "ipyvolume", "ipyleaflet", "xarray (<0.17)"]
@@ -837,7 +837,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
 [[package]]
 name = "pygments"
-version = "2.9.0"
+version = "2.10.0"
 description = "Pygments is a syntax highlighting package written in Python."
 category = "dev"
 optional = false
@@ -990,7 +990,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
 
 [[package]]
 name = "regex"
-version = "2021.7.6"
+version = "2021.8.3"
 description = "Alternative regular expression module, to replace re."
 category = "dev"
 optional = false
@@ -1169,7 +1169,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
 
 [[package]]
 name = "tomli"
-version = "1.1.0"
+version = "1.2.1"
 description = "A lil' TOML parser"
 category = "dev"
 optional = false
@@ -1185,7 +1185,7 @@ python-versions = ">= 3.5"
 
 [[package]]
 name = "tqdm"
-version = "4.61.2"
+version = "4.62.1"
 description = "Fast, Extensible Progress Meter"
 category = "main"
 optional = false
@@ -1223,7 +1223,7 @@ python-versions = "*"
 
 [[package]]
 name = "types-python-dateutil"
-version = "0.1.4"
+version = "0.1.6"
 description = "Typing stubs for python-dateutil"
 category = "dev"
 optional = false
@@ -1231,7 +1231,7 @@ python-versions = "*"
 
 [[package]]
 name = "types-pyyaml"
-version = "5.4.3"
+version = "5.4.6"
 description = "Typing stubs for PyYAML"
 category = "dev"
 optional = false
@@ -1239,7 +1239,7 @@ python-versions = "*"
 
 [[package]]
 name = "types-requests"
-version = "2.25.0"
+version = "2.25.6"
 description = "Typing stubs for requests"
 category = "dev"
 optional = false
@@ -1337,8 +1337,8 @@ appnope = [
     {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"},
 ]
 astroid = [
-    {file = "astroid-2.6.5-py3-none-any.whl", hash = "sha256:7b963d1c590d490f60d2973e57437115978d3a2529843f160b5003b721e1e925"},
-    {file = "astroid-2.6.5.tar.gz", hash = "sha256:83e494b02d75d07d4e347b27c066fd791c0c74fc96c613d1ea3de0c82c48168f"},
+    {file = "astroid-2.6.6-py3-none-any.whl", hash = "sha256:ab7f36e8a78b8e54a62028ba6beef7561db4cdb6f2a5009ecc44a6f42b5697ef"},
+    {file = "astroid-2.6.6.tar.gz", hash = "sha256:3975a0bd5373bdce166e60c851cfcbaf21ee96de80ec518c1f4cb3e94c3fb334"},
 ]
 atomicwrites = [
     {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
@@ -1373,8 +1373,8 @@ black = [
     {file = "black-21.7b0.tar.gz", hash = "sha256:c8373c6491de9362e39271630b65b964607bc5c79c83783547d76c839b3aa219"},
 ]
 bleach = [
-    {file = "bleach-3.3.1-py2.py3-none-any.whl", hash = "sha256:ae976d7174bba988c0b632def82fdc94235756edfb14e6558a9c5be555c9fb78"},
-    {file = "bleach-3.3.1.tar.gz", hash = "sha256:306483a5a9795474160ad57fce3ddd1b50551e981eed8e15a582d34cef28aafa"},
+    {file = "bleach-4.0.0-py2.py3-none-any.whl", hash = "sha256:c1685a132e6a9a38bf93752e5faab33a9517a6c0bb2f37b785e47bf253bdb51d"},
+    {file = "bleach-4.0.0.tar.gz", hash = "sha256:ffa9221c6ac29399cc50fcc33473366edd0cf8d5e2cbbbb63296dc327fb67cc8"},
 ]
 bokeh = [
     {file = "bokeh-2.3.3.tar.gz", hash = "sha256:a5fdcc181835561447fcc5a371300973fce4114692d5853addec284d1cdeb677"},
@@ -1384,8 +1384,8 @@ certifi = [
     {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"},
 ]
 charset-normalizer = [
-    {file = "charset-normalizer-2.0.3.tar.gz", hash = "sha256:c46c3ace2d744cfbdebceaa3c19ae691f53ae621b39fd7570f59d14fb7f2fd12"},
-    {file = "charset_normalizer-2.0.3-py3-none-any.whl", hash = "sha256:88fce3fa5b1a84fdcb3f603d889f723d1dd89b26059d0123ca435570e848d5e1"},
+    {file = "charset-normalizer-2.0.4.tar.gz", hash = "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"},
+    {file = "charset_normalizer-2.0.4-py3-none-any.whl", hash = "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b"},
 ]
 click = [
     {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"},
@@ -1494,16 +1494,16 @@ imagesize = [
     {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"},
 ]
 importlib-metadata = [
-    {file = "importlib_metadata-4.6.1-py3-none-any.whl", hash = "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"},
-    {file = "importlib_metadata-4.6.1.tar.gz", hash = "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac"},
+    {file = "importlib_metadata-4.6.4-py3-none-any.whl", hash = "sha256:ed5157fef23a4bc4594615a0dd8eba94b2bb36bf2a343fa3d8bb2fa0a62a99d5"},
+    {file = "importlib_metadata-4.6.4.tar.gz", hash = "sha256:7b30a78db2922d78a6f47fb30683156a14f3c6aa5cc23f77cc8967e9ab2d002f"},
 ]
 iniconfig = [
     {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
     {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
 ]
 ipython = [
-    {file = "ipython-7.25.0-py3-none-any.whl", hash = "sha256:aa21412f2b04ad1a652e30564fff6b4de04726ce875eab222c8430edc6db383a"},
-    {file = "ipython-7.25.0.tar.gz", hash = "sha256:54bbd1fe3882457aaf28ae060a5ccdef97f212a741754e420028d4ec5c2291dc"},
+    {file = "ipython-7.26.0-py3-none-any.whl", hash = "sha256:892743b65c21ed72b806a3a602cca408520b3200b89d1924f4b3d2cdb3692362"},
+    {file = "ipython-7.26.0.tar.gz", hash = "sha256:0cff04bb042800129348701f7bd68a430a844e8fb193979c08f6c99f28bb735e"},
 ]
 ipython-genutils = [
     {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"},
@@ -1745,8 +1745,8 @@ pandas = [
     {file = "pandas-1.2.5.tar.gz", hash = "sha256:14abb8ea73fce8aebbb1fb44bec809163f1c55241bcc1db91c2c780e97265033"},
 ]
 panel = [
-    {file = "panel-0.12.0-py2.py3-none-any.whl", hash = "sha256:1b79aa0515f306eab8a1c5ace6d3387ad54c6ec8ff210e53fe809c5b6e938373"},
-    {file = "panel-0.12.0.tar.gz", hash = "sha256:733bb6ce4ecbc6d589b471875c9f0b3e5540c9a5ea3c63ffa6f6b969989411e1"},
+    {file = "panel-0.12.1-py2.py3-none-any.whl", hash = "sha256:13ebcf0506fe7853ab3273ef91985f2b68944fa0ebf78393f1dcdb72c30f56d5"},
+    {file = "panel-0.12.1.tar.gz", hash = "sha256:e4898d60abdb82f8a429df7f59dbf8bcaf7e19b3e633555512ceb4ce06678458"},
 ]
 param = [
     {file = "param-1.11.1-py2.py3-none-any.whl", hash = "sha256:62fd2f679ed4f0dd2c146cf4b399ddc23e50df462e1c3080a3401747be56e195"},
@@ -1842,8 +1842,8 @@ pyflakes = [
     {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
 ]
 pygments = [
-    {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"},
-    {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"},
+    {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"},
+    {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"},
 ]
 pylint = [
     {file = "pylint-2.9.6-py3-none-any.whl", hash = "sha256:2e1a0eb2e8ab41d6b5dbada87f066492bb1557b12b76c47c2ee8aa8a11186594"},
@@ -1917,47 +1917,39 @@ pyyaml = [
     {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
 ]
 regex = [
-    {file = "regex-2021.7.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874"},
-    {file = "regex-2021.7.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854"},
-    {file = "regex-2021.7.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2"},
-    {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d"},
-    {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d"},
-    {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa"},
-    {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222"},
-    {file = "regex-2021.7.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407"},
-    {file = "regex-2021.7.6-cp36-cp36m-win32.whl", hash = "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b"},
-    {file = "regex-2021.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb"},
-    {file = "regex-2021.7.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d"},
-    {file = "regex-2021.7.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d"},
-    {file = "regex-2021.7.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0"},
-    {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec"},
-    {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694"},
-    {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a"},
-    {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad"},
-    {file = "regex-2021.7.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895"},
-    {file = "regex-2021.7.6-cp37-cp37m-win32.whl", hash = "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5"},
-    {file = "regex-2021.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f"},
-    {file = "regex-2021.7.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c"},
-    {file = "regex-2021.7.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0"},
-    {file = "regex-2021.7.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26"},
-    {file = "regex-2021.7.6-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f"},
-    {file = "regex-2021.7.6-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf"},
-    {file = "regex-2021.7.6-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd"},
-    {file = "regex-2021.7.6-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761"},
-    {file = "regex-2021.7.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068"},
-    {file = "regex-2021.7.6-cp38-cp38-win32.whl", hash = "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0"},
-    {file = "regex-2021.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4"},
-    {file = "regex-2021.7.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026"},
-    {file = "regex-2021.7.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417"},
-    {file = "regex-2021.7.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde"},
-    {file = "regex-2021.7.6-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59"},
-    {file = "regex-2021.7.6-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985"},
-    {file = "regex-2021.7.6-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d"},
-    {file = "regex-2021.7.6-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58"},
-    {file = "regex-2021.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3"},
-    {file = "regex-2021.7.6-cp39-cp39-win32.whl", hash = "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035"},
-    {file = "regex-2021.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c"},
-    {file = "regex-2021.7.6.tar.gz", hash = "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d"},
+    {file = "regex-2021.8.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8764a78c5464ac6bde91a8c87dd718c27c1cabb7ed2b4beaf36d3e8e390567f9"},
+    {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4551728b767f35f86b8e5ec19a363df87450c7376d7419c3cac5b9ceb4bce576"},
+    {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:577737ec3d4c195c4aef01b757905779a9e9aee608fa1cf0aec16b5576c893d3"},
+    {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c856ec9b42e5af4fe2d8e75970fcc3a2c15925cbcc6e7a9bcb44583b10b95e80"},
+    {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3835de96524a7b6869a6c710b26c90e94558c31006e96ca3cf6af6751b27dca1"},
+    {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cea56288eeda8b7511d507bbe7790d89ae7049daa5f51ae31a35ae3c05408531"},
+    {file = "regex-2021.8.3-cp36-cp36m-win32.whl", hash = "sha256:a4eddbe2a715b2dd3849afbdeacf1cc283160b24e09baf64fa5675f51940419d"},
+    {file = "regex-2021.8.3-cp36-cp36m-win_amd64.whl", hash = "sha256:57fece29f7cc55d882fe282d9de52f2f522bb85290555b49394102f3621751ee"},
+    {file = "regex-2021.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a5c6dbe09aff091adfa8c7cfc1a0e83fdb8021ddb2c183512775a14f1435fe16"},
+    {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff4a8ad9638b7ca52313d8732f37ecd5fd3c8e3aff10a8ccb93176fd5b3812f6"},
+    {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b63e3571b24a7959017573b6455e05b675050bbbea69408f35f3cb984ec54363"},
+    {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fbc20975eee093efa2071de80df7f972b7b35e560b213aafabcec7c0bd00bd8c"},
+    {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14caacd1853e40103f59571f169704367e79fb78fac3d6d09ac84d9197cadd16"},
+    {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb350eb1060591d8e89d6bac4713d41006cd4d479f5e11db334a48ff8999512f"},
+    {file = "regex-2021.8.3-cp37-cp37m-win32.whl", hash = "sha256:18fdc51458abc0a974822333bd3a932d4e06ba2a3243e9a1da305668bd62ec6d"},
+    {file = "regex-2021.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:026beb631097a4a3def7299aa5825e05e057de3c6d72b139c37813bfa351274b"},
+    {file = "regex-2021.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:16d9eaa8c7e91537516c20da37db975f09ac2e7772a0694b245076c6d68f85da"},
+    {file = "regex-2021.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3905c86cc4ab6d71635d6419a6f8d972cab7c634539bba6053c47354fd04452c"},
+    {file = "regex-2021.8.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937b20955806381e08e54bd9d71f83276d1f883264808521b70b33d98e4dec5d"},
+    {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:28e8af338240b6f39713a34e337c3813047896ace09d51593d6907c66c0708ba"},
+    {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c09d88a07483231119f5017904db8f60ad67906efac3f1baa31b9b7f7cca281"},
+    {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:85f568892422a0e96235eb8ea6c5a41c8ccbf55576a2260c0160800dbd7c4f20"},
+    {file = "regex-2021.8.3-cp38-cp38-win32.whl", hash = "sha256:bf6d987edd4a44dd2fa2723fca2790f9442ae4de2c8438e53fcb1befdf5d823a"},
+    {file = "regex-2021.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:8fe58d9f6e3d1abf690174fd75800fda9bdc23d2a287e77758dc0e8567e38ce6"},
+    {file = "regex-2021.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7976d410e42be9ae7458c1816a416218364e06e162b82e42f7060737e711d9ce"},
+    {file = "regex-2021.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9569da9e78f0947b249370cb8fadf1015a193c359e7e442ac9ecc585d937f08d"},
+    {file = "regex-2021.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459bbe342c5b2dec5c5223e7c363f291558bc27982ef39ffd6569e8c082bdc83"},
+    {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f421e3cdd3a273bace013751c345f4ebeef08f05e8c10757533ada360b51a39"},
+    {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea212df6e5d3f60341aef46401d32fcfded85593af1d82b8b4a7a68cd67fdd6b"},
+    {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a3b73390511edd2db2d34ff09aa0b2c08be974c71b4c0505b4a048d5dc128c2b"},
+    {file = "regex-2021.8.3-cp39-cp39-win32.whl", hash = "sha256:f35567470ee6dbfb946f069ed5f5615b40edcbb5f1e6e1d3d2b114468d505fc6"},
+    {file = "regex-2021.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:bfa6a679410b394600eafd16336b2ce8de43e9b13f7fb9247d84ef5ad2b45e91"},
+    {file = "regex-2021.8.3.tar.gz", hash = "sha256:8935937dad2c9b369c3d932b0edbc52a62647c2afb2fafc0c280f14a8bf56a6a"},
 ]
 requests = [
     {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
@@ -2015,8 +2007,8 @@ toml = [
     {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
 ]
 tomli = [
-    {file = "tomli-1.1.0-py3-none-any.whl", hash = "sha256:f4a182048010e89cbec0ae4686b21f550a7f2903f665e34a6de58ec15424f919"},
-    {file = "tomli-1.1.0.tar.gz", hash = "sha256:33d7984738f8bb699c9b0a816eb646a8178a69eaa792d258486776a5d21b8ca5"},
+    {file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"},
+    {file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"},
 ]
 tornado = [
     {file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"},
@@ -2062,8 +2054,8 @@ tornado = [
     {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"},
 ]
 tqdm = [
-    {file = "tqdm-4.61.2-py2.py3-none-any.whl", hash = "sha256:5aa445ea0ad8b16d82b15ab342de6b195a722d75fc1ef9934a46bba6feafbc64"},
-    {file = "tqdm-4.61.2.tar.gz", hash = "sha256:8bb94db0d4468fea27d004a0f1d1c02da3cdedc00fe491c0de986b76a04d6b0a"},
+    {file = "tqdm-4.62.1-py2.py3-none-any.whl", hash = "sha256:07856e19a1fe4d2d9621b539d3f072fa88c9c1ef1f3b7dd4d4953383134c3164"},
+    {file = "tqdm-4.62.1.tar.gz", hash = "sha256:35540feeaca9ac40c304e916729e6b78045cbbeccd3e941b2868f09306798ac9"},
 ]
 traitlets = [
     {file = "traitlets-5.0.5-py3-none-any.whl", hash = "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426"},
@@ -2102,16 +2094,16 @@ typed-ast = [
     {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
 ]
 types-python-dateutil = [
-    {file = "types-python-dateutil-0.1.4.tar.gz", hash = "sha256:e6486ca27b6dde73e0ec079a9e1b03e208766e6bc7f1e08964a7e9104a5c7d7a"},
-    {file = "types_python_dateutil-0.1.4-py2.py3-none-any.whl", hash = "sha256:39bfe0bde61fc673b8fa28167bd78622d976210f791971b9f3e10877cbf119a4"},
+    {file = "types-python-dateutil-0.1.6.tar.gz", hash = "sha256:b02de39a54ce6e3fadfdc7dba77d8519fbfb6ca049920e190b5f89c74d5f9de6"},
+    {file = "types_python_dateutil-0.1.6-py3-none-any.whl", hash = "sha256:5b6241ea9fca2d8878cc152017d9524da62a7a856b98e31006e68b02aab47442"},
 ]
 types-pyyaml = [
-    {file = "types-PyYAML-5.4.3.tar.gz", hash = "sha256:2e7b81b2b7af751634425107b986086c6ba7cb61270a43a5c290c58be8cdbc3a"},
-    {file = "types_PyYAML-5.4.3-py2.py3-none-any.whl", hash = "sha256:bca83cbfc0be48600a8abf1e3d87fb762a91e6d35d724029a3321dd2dce2ceb1"},
+    {file = "types-PyYAML-5.4.6.tar.gz", hash = "sha256:745dcb4b1522423026bcc83abb9925fba747f1e8602d902f71a4058f9e7fb662"},
+    {file = "types_PyYAML-5.4.6-py3-none-any.whl", hash = "sha256:96f8d3d96aa1a18a465e8f6a220e02cff2f52632314845a364ecbacb0aea6e30"},
 ]
 types-requests = [
-    {file = "types-requests-2.25.0.tar.gz", hash = "sha256:ee0d0c507210141b7d5b8639cc43eaa726084178775db2a5fb06fbf85c185808"},
-    {file = "types_requests-2.25.0-py3-none-any.whl", hash = "sha256:fa5c1e5e832ff6193507d8da7e1159281383908ee193a2f4b37bc08140b51844"},
+    {file = "types-requests-2.25.6.tar.gz", hash = "sha256:e21541c0f55c066c491a639309159556dd8c5833e49fcde929c4c47bdb0002ee"},
+    {file = "types_requests-2.25.6-py3-none-any.whl", hash = "sha256:a5a305b43ea57bf64d6731f89816946a405b591eff6de28d4c0fd58422cee779"},
 ]
 typing-extensions = [
     {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"},
diff --git a/pyproject.toml b/pyproject.toml
index 9ad71bb1..145ad7c0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "annofabcli"
-version = "1.45.0"
+version = "1.46.0"
 description = "Utility Command Line Interface for AnnoFab"
 authors = ["yuji38kwmt"]
 license = "MIT"


Test patch:
diff --git a/tests/test_inspection_comment.py b/tests/test_inspection_comment.py
index 77124e32..33c53b47 100644
--- a/tests/test_inspection_comment.py
+++ b/tests/test_inspection_comment.py
@@ -1,7 +1,10 @@
 import configparser
+import json
 import os
 from pathlib import Path
 
+import annofabapi
+
 from annofabcli.__main__ import main
 
 out_dir = Path("./tests/out/inspection_comment")
@@ -18,6 +21,16 @@
 
 
 class TestCommandLine:
+    @classmethod
+    def setup_class(cls):
+        annofab_service = annofabapi.build()
+        task, _ = annofab_service.api.get_task(project_id, task_id)
+        cls.input_data_id = task["input_data_id_list"][0]
+
+    def test_delete_inspection_comment(self):
+        dict_comments = {task_id: {self.input_data_id: ["foo", "bar"]}}
+        main(["inspection_comment", "delete", "--project_id", project_id, "--json", json.dumps(dict_comments), "--yes"])
+
     def test_list_inspection_comment(self):
         main(
             [
@@ -44,3 +57,17 @@ def test_list_inspection_comment_with_json(self):
                 str(out_dir / "list_with_json-out.csv"),
             ]
         )
+
+    def test_put_inspection_comment(self):
+        dict_comments = {
+            task_id: {
+                self.input_data_id: [
+                    {
+                        "comment": "test comment",
+                        "data": {"x": 10, "y": 20, "_type": "Point"},
+                    }
+                ]
+            }
+        }
+
+        main(["inspection_comment", "put", "--project_id", project_id, "--json", json.dumps(dict_comments), "--yes"])