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 ]
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
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
def __init__(self, service: annofabapi.Resource, facade: AnnofabApiFacade, args: argparse.Namespace): super().__init__(service, facade, args) self.visualize = AddProps(self.service, args.project_id)
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, )