def filter_inspection_list(
        self,
        project_id: str,
        inspection_comment_list: List[Inspection],
        task_id_list: Optional[List[str]] = None,
        filter_inspection_comment: Optional[FilterInspectionFunc] = None,
    ) -> List[Inspection]:
        """
        引数の検査コメント一覧に`commenter_username`など、ユーザが知りたい情報を追加する。

        Args:
            inspection_list: 検査コメント一覧
            filter_inspection: 検索コメントを絞り込むための関数

        Returns:
            情報が追加された検査コメント一覧
        """
        def filter_task_id(e):
            if task_id_list is None or len(task_id_list) == 0:
                return True
            return e["task_id"] in task_id_list

        def filter_local_inspection_comment(e):
            if filter_inspection_comment is None:
                return True
            return filter_inspection_comment(e)

        inspection_list = [
            e for e in inspection_comment_list
            if filter_local_inspection_comment(e) and filter_task_id(e)
        ]
        visualize = AddProps(self.service, project_id)
        return [
            visualize.add_properties_to_inspection(e) for e in inspection_list
        ]
示例#2
0
    def get_target_attributes_columns(
        annotation_specs_labels: List[Dict[str,
                                           Any]]) -> List[AttributesColumn]:
        """
        出力対象の属性情報を取得する(label, attribute, choice)
        """

        target_attributes_columns: List[AttributesColumn] = []
        for label in annotation_specs_labels:
            label_name_en = AddProps.get_message(label["label_name"],
                                                 MessageLocale.EN)
            label_name_en = label_name_en if label_name_en is not None else ""

            for attribute in label["additional_data_definitions"]:
                attribute_name_en = AddProps.get_message(
                    attribute["name"], MessageLocale.EN)
                attribute_name_en = attribute_name_en if attribute_name_en is not None else ""

                if AdditionalDataDefinitionType(attribute["type"]) in [
                        AdditionalDataDefinitionType.CHOICE,
                        AdditionalDataDefinitionType.SELECT,
                ]:
                    for choice in attribute["choices"]:
                        choice_name_en = AddProps.get_message(
                            choice["name"], MessageLocale.EN)
                        choice_name_en = choice_name_en if choice_name_en is not None else ""
                        target_attributes_columns.append(
                            (label_name_en, attribute_name_en, choice_name_en))

                elif AdditionalDataDefinitionType(
                        attribute["type"]
                ) == AdditionalDataDefinitionType.FLAG:
                    target_attributes_columns.append(
                        (label_name_en, attribute_name_en, "True"))
                    target_attributes_columns.append(
                        (label_name_en, attribute_name_en, "False"))

                else:
                    continue

        return target_attributes_columns
示例#3
0
 def to_label_name(label: Dict[str, Any]) -> str:
     label_name_en = AddProps.get_message(label["label_name"],
                                          MessageLocale.EN)
     label_name_en = label_name_en if label_name_en is not None else ""
     return label_name_en
示例#4
0
 def __init__(self, service: annofabapi.Resource, facade: AnnofabApiFacade, args: argparse.Namespace):
     super().__init__(service, facade, args)
     self.visualize = AddProps(self.service, args.project_id)
示例#5
0
class ImportAnnotation(AbstractCommandLineInterface):
    """
    アノテーションをインポートする
    """

    def __init__(self, service: annofabapi.Resource, facade: AnnofabApiFacade, args: argparse.Namespace):
        super().__init__(service, facade, args)
        self.visualize = AddProps(self.service, args.project_id)

    def get_label_info_from_label_name(self, label_name: str) -> Optional[LabelV1]:
        for label in self.visualize.specs_labels:
            label_name_en = self.visualize.get_label_name(label["label_id"], MessageLocale.EN)
            if label_name_en is not None and label_name_en == label_name:
                return label

        logger.warning(f"アノテーション仕様に label_name={label_name} のラベルが存在しません。")
        return None

    def _get_additional_data_from_attribute_name(
        self, attribute_name: str, label_info: LabelV1
    ) -> Optional[AdditionalDataDefinitionV1]:
        for additional_data in label_info["additional_data_definitions"]:
            additional_data_name_en = self.visualize.get_additional_data_name(
                additional_data["additional_data_definition_id"], MessageLocale.EN, label_id=label_info["label_id"]
            )
            if additional_data_name_en is not None and additional_data_name_en == attribute_name:
                return additional_data

        return None

    def _get_choice_id_from_name(self, name: str, choices: List[Dict[str, Any]]) -> Optional[str]:
        choice_info = first_true(choices, pred=lambda e: self.facade.get_choice_name_en(e) == name)
        if choice_info is not None:
            return choice_info["choice_id"]
        else:
            return None

    @staticmethod
    def _get_data_holding_type_from_data(data: FullAnnotationData) -> AnnotationDataHoldingType:
        if data["_type"] in ["Segmentation", "SegmentationV2"]:
            return AnnotationDataHoldingType.OUTER
        else:
            return AnnotationDataHoldingType.INNER

    def _to_additional_data_list(self, attributes: Dict[str, Any], label_info: LabelV1) -> List[AdditionalData]:
        additional_data_list: List[AdditionalData] = []
        for key, value in attributes.items():
            specs_additional_data = self._get_additional_data_from_attribute_name(key, label_info)
            if specs_additional_data is None:
                logger.warning(f"アノテーション仕様に attribute_name={key} が存在しません。")
                continue

            additional_data = AdditionalData(
                additional_data_definition_id=specs_additional_data["additional_data_definition_id"],
                flag=None,
                integer=None,
                choice=None,
                comment=None,
            )
            additional_data_type = AdditionalDataDefinitionType(specs_additional_data["type"])
            if additional_data_type == AdditionalDataDefinitionType.FLAG:
                additional_data.flag = value
            elif additional_data_type == AdditionalDataDefinitionType.INTEGER:
                additional_data.integer = value
            elif additional_data_type in [
                AdditionalDataDefinitionType.TEXT,
                AdditionalDataDefinitionType.COMMENT,
                AdditionalDataDefinitionType.TRACKING,
                AdditionalDataDefinitionType.LINK,
            ]:
                additional_data.comment = value
            elif additional_data_type in [AdditionalDataDefinitionType.CHOICE, AdditionalDataDefinitionType.SELECT]:
                additional_data.choice = self._get_choice_id_from_name(value, specs_additional_data["choices"])
            else:
                logger.warning(f"additional_data_type={additional_data_type}が不正です。")
                continue

            additional_data_list.append(additional_data)

        return additional_data_list

    def _to_annotation_detail_for_request(
        self, project_id: str, parser: SimpleAnnotationParser, detail: ImportedSimpleAnnotationDetail, now_datetime: str
    ) -> Optional[AnnotationDetail]:
        """
        Request Bodyに渡すDataClassに変換する。塗りつぶし画像があれば、それをS3にアップロードする。

        Args:
            project_id:
            parser:
            detail:

        Returns:
            変換できない場合はNoneを返す

        """
        label_info = self.get_label_info_from_label_name(detail.label)
        if label_info is None:
            return None

        def _get_annotation_id(arg_label_info: LabelV1) -> str:
            if detail.annotation_id is not None:
                return detail.annotation_id
            else:
                if arg_label_info["annotation_type"] == AnnotationType.CLASSIFICATION.value:
                    # 全体アノテーションの場合、annotation_idはlabel_idである必要がある
                    return arg_label_info["label_id"]
                else:
                    return str(uuid.uuid4())

        additional_data_list: List[AdditionalData] = self._to_additional_data_list(detail.attributes, label_info)
        data_holding_type = self._get_data_holding_type_from_data(detail.data)

        dest_obj = AnnotationDetail(
            label_id=label_info["label_id"],
            annotation_id=_get_annotation_id(label_info),
            account_id=self.service.api.account_id,
            data_holding_type=data_holding_type,
            data=detail.data,
            additional_data_list=additional_data_list,
            is_protected=False,
            etag=None,
            url=None,
            path=None,
            created_datetime=now_datetime,
            updated_datetime=now_datetime,
        )

        if data_holding_type == AnnotationDataHoldingType.OUTER:
            data_uri = detail.data["data_uri"]
            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")
                dest_obj.path = s3_path
                logger.debug(f"{parser.task_id}/{parser.input_data_id}/{data_uri} をS3にアップロードしました。")

        return dest_obj

    def parser_to_request_body(
        self,
        project_id: str,
        parser: SimpleAnnotationParser,
        details: List[ImportedSimpleAnnotationDetail],
        old_annotation: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        request_details: List[Dict[str, Any]] = []
        now_datetime = str_now()
        for detail in details:
            request_detail = self._to_annotation_detail_for_request(
                project_id, parser, detail, now_datetime=now_datetime
            )

            if request_detail is not None:
                request_details.append(request_detail.to_dict(encode_json=True))

        updated_datetime = old_annotation["updated_datetime"] if old_annotation is not None else None

        request_body = {
            "project_id": project_id,
            "task_id": parser.task_id,
            "input_data_id": parser.input_data_id,
            "details": request_details,
            "updated_datetime": updated_datetime,
        }

        return request_body

    def put_annotation_for_input_data(
        self, project_id: str, parser: SimpleAnnotationParser, overwrite: bool = False
    ) -> bool:

        task_id = parser.task_id
        input_data_id = parser.input_data_id

        simple_annotation: ImportedSimpleAnnotation = ImportedSimpleAnnotation.from_dict(parser.load_json())
        if len(simple_annotation.details) == 0:
            logger.debug(
                f"task_id={task_id}, input_data_id={input_data_id} : インポート元にアノテーションデータがないため、アノテーションの登録をスキップします。"
            )
            return False

        input_data = self.service.wrapper.get_input_data_or_none(project_id, input_data_id)
        if input_data is None:
            logger.warning(f"task_id= '{task_id}, input_data_id = '{input_data_id}' は存在しません。")
            return False

        old_annotation, _ = self.service.api.get_editor_annotation(project_id, task_id, input_data_id)
        if len(old_annotation["details"]) > 0 and not overwrite:
            logger.debug(
                f"task_id={task_id}, input_data_id={input_data_id} : "
                f"インポート先のタスクに既にアノテーションが存在するため、アノテーションの登録をスキップします。"
                f"アノテーションを上書きする場合は、`--overwrite` を指定してください。"
            )
            return False

        logger.info(f"task_id={task_id}, input_data_id={input_data_id} : アノテーションを登録します。")
        request_body = self.parser_to_request_body(
            project_id, parser, simple_annotation.details, old_annotation=old_annotation
        )

        self.service.api.put_annotation(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, overwrite: bool
    ) -> 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, overwrite=overwrite):
                    success_count += 1
            except Exception as e:  # pylint: disable=broad-except
                logger.warning(
                    f"task_id={parser.task_id}, input_data_id={parser.input_data_id} の"
                    f"アノテーションインポートに失敗しました。: {type(e).__name__}: {e}"
                )

        logger.info(f"タスク'{task_parser.task_id}'の入力データ {success_count} 個に対してアノテーションをインポートしました。")
        return success_count

    def execute_task(
        self, project_id: str, task_parser: SimpleAnnotationParserByTask, overwrite: bool, force: bool
    ) -> bool:
        """
        1個のタスクに対してアノテーションを登録する。

        Args:
            project_id:
            task_parser:
            overwrite:

        Returns:
            1個以上の入力データのアノテーションを変更したか

        """
        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} に対して処理します。")

        task = self.service.wrapper.get_task_or_none(project_id, task_id)
        if task is None:
            logger.warning(f"task_id = '{task_id}' は存在しません。")
            return False

        if task["status"] in [TaskStatus.WORKING.value, TaskStatus.COMPLETE.value]:
            logger.info(f"タスク'{task_id}'は作業中または受入完了状態のため、インポートをスキップします。 status={task['status']}")
            return False

        old_account_id: Optional[str] = None
        changed_operator = False
        if 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
                )
                changed_operator = True
                old_account_id = task["account_id"]

        else:
            if not can_put_annotation(task, self.service.api.account_id):
                logger.debug(
                    f"タスク'{task_id}'は、過去に誰かに割り当てられたタスクで、現在の担当者が自分自身でないため、アノテーションのインポートをスキップします。"
                    f"担当者を自分自身に変更してアノテーションを登録する場合は `--force` を指定してください。"
                )
                return False

        result_count = self.put_annotation_for_task(project_id, task_parser, overwrite)
        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)

        return result_count > 0

    @staticmethod
    def validate(args: argparse.Namespace) -> bool:
        COMMON_MESSAGE = "annofabcli annotation import: error:"
        annotation_path = Path(args.annotation)
        if not annotation_path.exists():
            print(
                f"{COMMON_MESSAGE} argument --annotation: ZIPファイルまたはディレクトリが存在しません。'{str(annotation_path)}'",
                file=sys.stderr,
            )
            return False

        elif annotation_path.is_file() and not zipfile.is_zipfile(str(annotation_path)):
            print(f"{COMMON_MESSAGE} argument --annotation: ZIPファイルまたはディレクトリを指定してください。", file=sys.stderr)
            return False

        return True

    def main(self):
        args = self.args
        if not self.validate(args):
            return

        project_id = args.project_id
        annotation_path = Path(args.annotation)

        super().validate_project(project_id, [ProjectMemberRole.OWNER])

        task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id)

        # Simpleアノテーションの読み込み
        if annotation_path.is_file():
            iter_task_parser = lazy_parse_simple_annotation_zip_by_task(annotation_path)
        else:
            iter_task_parser = lazy_parse_simple_annotation_dir_by_task(annotation_path)

        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, overwrite=args.overwrite, force=args.force):
                            success_count += 1
                else:
                    # コマンドライン引数で --task_idが指定されていない場合はすべてをインポートする
                    if self.execute_task(project_id, task_parser, overwrite=args.overwrite, force=args.force):
                        success_count += 1

            except Exception as e:  # pylint: disable=broad-except
                logger.warning(f"task_id={task_parser.task_id} のアノテーションインポートに失敗しました。: {type(e).__name__}: {e}")

        logger.info(f"{success_count} 個のタスクに対してアノテーションをインポートしました。")
class PrintInspections(AbstractCommandLineInterface):
    """
    検査コメント一覧を出力する。
    """

    def __init__(self, service: annofabapi.Resource, facade: AnnofabApiFacade, args: argparse.Namespace):
        super().__init__(service, facade, args)
        self.visualize = AddProps(self.service, args.project_id)

    def filter_inspection_list(
        self,
        inspection_list: List[Inspection],
        task_id_list: Optional[List[str]] = None,
        arg_filter_inspection: Optional[FilterInspectionFunc] = None,
    ) -> List[Inspection]:
        """
        引数の検査コメント一覧に`commenter_username`など、ユーザが知りたい情報を追加する。

        Args:
            inspection_list: 検査コメント一覧
            filter_inspection: 検索コメントを絞り込むための関数

        Returns:
            情報が追加された検査コメント一覧
        """

        def filter_task_id(e):
            if task_id_list is None or len(task_id_list) == 0:
                return True
            return e["task_id"] in task_id_list

        def filter_inspection(e):
            if arg_filter_inspection is None:
                return True
            return arg_filter_inspection(e)

        inspection_list = [e for e in inspection_list if filter_inspection(e) and filter_task_id(e)]
        return [self.visualize.add_properties_to_inspection(e) for e in inspection_list]

    def print_inspections(
        self,
        project_id: str,
        task_id_list: List[str],
        filter_inspection: Optional[FilterInspectionFunc] = None,
    ):
        """
        検査コメントを出力する

        Args:
            project_id: 対象のproject_id
            task_id_list: 受け入れ完了にするタスクのtask_idのList
            filter_inspection: 検索コメントを絞り込むための関数

        Returns:

        """

        inspection_list = self.get_inspections(
            project_id, task_id_list=task_id_list, filter_inspection=filter_inspection
        )

        logger.info(f"検査コメントの件数: {len(inspection_list)}")

        self.print_according_to_format(inspection_list)

    def get_inspections_by_input_data(self, project_id: str, task_id: str, input_data_id: str, input_data_index: int):
        """入力データごとに検査コメント一覧を取得する。

        Args:
            project_id:
            task_id:
            input_data_id:
            input_data_index: タスク内のinput_dataの番号

        Returns:
            対象の検査コメント一覧
        """

        detail = {"input_data_index": input_data_index}
        inspectins, _ = self.service.api.get_inspections(project_id, task_id, input_data_id)
        return [self.visualize.add_properties_to_inspection(e, detail) for e in inspectins]

    def get_inspections(
        self, project_id: str, task_id_list: List[str], filter_inspection: Optional[FilterInspectionFunc] = None
    ) -> List[Inspection]:
        """検査コメント一覧を取得する。

        Args:
            project_id:
            task_id_list:

        Returns:
            対象の検査コメント一覧
        """

        all_inspections: List[Inspection] = []
        for task_id in task_id_list:
            try:
                task, _ = self.service.api.get_task(project_id, task_id)
                input_data_id_list = task["input_data_id_list"]
                logger.info(f"タスク '{task_id}' に紐づく検査コメントを取得します。input_dataの個数 = {len(input_data_id_list)}")
                for input_data_index, input_data_id in enumerate(input_data_id_list):

                    inspections = self.get_inspections_by_input_data(
                        project_id, task_id, input_data_id, input_data_index
                    )

                    if filter_inspection is not None:
                        inspections = [e for e in inspections if filter_inspection(e)]

                    all_inspections.extend(inspections)

            except requests.HTTPError as e:
                logger.warning(e)
                logger.warning(f"タスク task_id = {task_id} の検査コメントを取得できなかった。")

        return all_inspections

    def main(self):
        args = self.args
        task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id)

        filter_inspection = create_filter_func(only_reply=args.only_reply, exclude_reply=args.exclude_reply)
        self.print_inspections(
            args.project_id,
            task_id_list,
            filter_inspection=filter_inspection,
        )