diff --git a/annofabcli/__version__.py b/annofabcli/__version__.py index 4b6e902a..7f6f982e 100644 --- a/annofabcli/__version__.py +++ b/annofabcli/__version__.py @@ -1 +1 @@ -__version__ = "1.73.3" +__version__ = "1.74.0" diff --git a/annofabcli/statistics/table.py b/annofabcli/statistics/table.py index 76044f98..b2f6a3f8 100644 --- a/annofabcli/statistics/table.py +++ b/annofabcli/statistics/table.py @@ -9,7 +9,7 @@ from annofabcli.common.facade import AnnofabApiFacade from annofabcli.common.utils import isoduration_to_hour from annofabcli.statistics.database import Database -from annofabcli.task.list_tasks_added_task_history import AddingAdditionalInfoToTask +from annofabcli.task.list_all_tasks_added_task_history import AddingAdditionalInfoToTask logger = logging.getLogger(__name__) diff --git a/annofabcli/task/list_all_tasks_added_task_history.py b/annofabcli/task/list_all_tasks_added_task_history.py new file mode 100644 index 00000000..1f666251 --- /dev/null +++ b/annofabcli/task/list_all_tasks_added_task_history.py @@ -0,0 +1,224 @@ +from __future__ import annotations + +import argparse +import json +import logging +import sys +import tempfile +from pathlib import Path +from typing import Any, Dict, List, Optional, Set + +import annofabapi +from annofabapi.models import ProjectMemberRole, TaskHistory + +import annofabcli +from annofabcli.common.cli import ( + COMMAND_LINE_ERROR_STATUS_CODE, + AbstractCommandLineInterface, + ArgumentParser, + build_annofabapi_resource_and_login, +) +from annofabcli.common.dataclasses import WaitOptions +from annofabcli.common.download import DownloadingFile +from annofabcli.common.enums import FormatArgument +from annofabcli.common.facade import AnnofabApiFacade, TaskQuery, match_task_with_query +from annofabcli.task.list_tasks_added_task_history import AddingAdditionalInfoToTask, TasksAddedTaskHistoryOutput + +logger = logging.getLogger(__name__) + + +TaskHistoryDict = Dict[str, List[TaskHistory]] +"""タスク履歴の辞書(key: task_id, value: タスク履歴一覧)""" + +DEFAULT_WAIT_OPTIONS = WaitOptions(interval=60, max_tries=360) + + +class ListAllTasksAddedTaskHistoryMain: + def __init__(self, service: annofabapi.Resource, project_id: str): + self.service = service + self.project_id = project_id + self.downloading_obj = DownloadingFile(self.service) + self.facade = AnnofabApiFacade(self.service) + + def get_detail_task_list( + self, + task_list: list[dict[str, Any]], + task_history_dict: TaskHistoryDict, + ): + obj = AddingAdditionalInfoToTask(self.service, project_id=self.project_id) + + for task in task_list: + + obj.add_additional_info_to_task(task) + + task_id = task["task_id"] + task_histories = task_history_dict.get(task_id) + if task_histories is None: + logger.warning(f"task_id='{task_id}' に紐づくタスク履歴情報は存在しないので、タスク履歴の付加的情報はタスクに追加しません。") + continue + obj.add_task_history_additional_info_to_task(task, task_histories) + + return task_list + + def load_task_list(self, task_json_path: Optional[Path]) -> list[dict[str, Any]]: + if task_json_path is not None: + with task_json_path.open(encoding="utf-8") as f: + return json.load(f) + + with tempfile.NamedTemporaryFile() as tmp_file: + self.downloading_obj.download_task_json(self.project_id, tmp_file.name) + with open(tmp_file.name, encoding="utf-8") as f: + return json.load(f) + + def load_task_history_dict(self, task_history_json_path: Optional[Path]) -> TaskHistoryDict: + if task_history_json_path is not None: + with task_history_json_path.open(encoding="utf-8") as f: + return json.load(f) + else: + with tempfile.NamedTemporaryFile() as tmp_file: + self.downloading_obj.download_task_history_json(self.project_id, tmp_file.name) + with open(tmp_file.name, encoding="utf-8") as f: + return json.load(f) + + @staticmethod + def match_task_with_conditions( + task: Dict[str, Any], + task_id_set: Optional[Set[str]] = None, + task_query: Optional[TaskQuery] = None, + ) -> bool: + result = True + + dc_task = annofabapi.dataclass.task.Task.from_dict(task) + result = result and match_task_with_query(dc_task, task_query) + if task_id_set is not None: + result = result and (dc_task.task_id in task_id_set) + return result + + def filter_task_list( + self, + task_list: List[Dict[str, Any]], + task_id_list: Optional[List[str]] = None, + task_query: Optional[TaskQuery] = None, + ) -> List[Dict[str, Any]]: + if task_query is not None: + task_query = self.facade.set_account_id_of_task_query(self.project_id, task_query) + + task_id_set = set(task_id_list) if task_id_list is not None else None + logger.debug(f"出力対象のタスクを抽出しています。") + filtered_task_list = [ + e for e in task_list if self.match_task_with_conditions(e, task_query=task_query, task_id_set=task_id_set) + ] + return filtered_task_list + + def get_task_list_added_task_history( + self, + task_json_path: Optional[Path], + task_history_json_path: Optional[Path], + task_id_list: Optional[list[str]], + task_query: Optional[TaskQuery], + ): + """ + タスク履歴情報を加えたタスク一覧を取得する。 + """ + task_list = self.load_task_list(task_json_path) + task_history_dict = self.load_task_history_dict(task_history_json_path) + + filtered_task_list = self.filter_task_list(task_list, task_id_list=task_id_list, task_query=task_query) + + logger.debug(f"タスク履歴に関する付加的情報を取得しています。") + detail_task_list = self.get_detail_task_list(task_list=filtered_task_list, task_history_dict=task_history_dict) + return detail_task_list + + +class ListAllTasksAddedTaskHistory(AbstractCommandLineInterface): + """ + タスクの一覧を表示する + """ + + @staticmethod + def validate(args: argparse.Namespace) -> bool: + COMMON_MESSAGE = "annofabcli task list_merged_task_history: error:" + if (args.task_json is None and args.task_history_json is not None) or ( + args.task_json is not None and args.task_history_json is None + ): + print( + f"{COMMON_MESSAGE} '--task_json'と'--task_history_json'の両方を指定する必要があります。", + file=sys.stderr, + ) + return False + + return True + + def main(self): + args = self.args + if not self.validate(args): + sys.exit(COMMAND_LINE_ERROR_STATUS_CODE) + + project_id = args.project_id + + task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id) if args.task_id is not None else None + task_query = ( + TaskQuery.from_dict(annofabcli.common.cli.get_json_from_args(args.task_query)) + if args.task_query is not None + else None + ) + + self.validate_project(project_id, [ProjectMemberRole.OWNER, ProjectMemberRole.TRAINING_DATA_USER]) + + task_list = ListAllTasksAddedTaskHistoryMain(self.service, project_id).get_task_list_added_task_history( + task_json_path=args.task_json, + task_history_json_path=args.task_history_json, + task_id_list=task_id_list, + task_query=task_query, + ) + + logger.info(f"タスク一覧の件数: {len(task_list)}") + TasksAddedTaskHistoryOutput(task_list).output( + output_path=args.output, output_format=FormatArgument(args.format) + ) + + +def main(args): + service = build_annofabapi_resource_and_login(args) + facade = AnnofabApiFacade(service) + ListAllTasksAddedTaskHistory(service, facade, args).main() + + +def parse_args(parser: argparse.ArgumentParser): + argument_parser = ArgumentParser(parser) + argument_parser.add_project_id() + argument_parser.add_task_query() + argument_parser.add_task_id(required=False) + + parser.add_argument( + "--task_json", + type=str, + help="タスク情報が記載されたJSONファイルのパスを指定すると、JSONに記載された情報を元に出力します。指定しない場合はJSONファイルをダウンロードします。\n" + "JSONファイルは ``$ annofabcli task download`` コマンドで取得できます。", + ) + + parser.add_argument( + "--task_history_json", + type=str, + help="タスク履歴情報が記載されたJSONファイルのパスを指定すると、JSONに記載された情報を元に出力します。指定しない場合はJSONファイルをダウンロードします。\n" + "JSONファイルは ``$ annofabcli task_history download`` コマンドで取得できます。", + ) + + argument_parser.add_output() + + argument_parser.add_format( + choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON], + default=FormatArgument.CSV, + ) + + parser.set_defaults(subcommand_func=main) + + +def add_parser(subparsers: Optional[argparse._SubParsersAction] = None): + subcommand_name = "list_all_added_task_history" + subcommand_help = "タスク履歴に関する情報を加えたタスク一覧のすべてを出力します。" + description = "タスク履歴に関する情報(フェーズごとの作業時間、担当者、開始日時)を加えたタスク一覧のすべてを出力します。" + epilog = "アノテーションユーザ/オーナロールを持つユーザで実行してください。" + parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog) + parse_args(parser) + return parser diff --git a/annofabcli/task/list_tasks.py b/annofabcli/task/list_tasks.py index cb94d69f..3bbc63bc 100644 --- a/annofabcli/task/list_tasks.py +++ b/annofabcli/task/list_tasks.py @@ -119,6 +119,7 @@ def get_task_list_with_api( def get_task_list( self, project_id: str, + *, task_id_list: Optional[List[str]] = None, task_query: Optional[Dict[str, Any]] = None, user_id_list: Optional[List[str]] = None, diff --git a/annofabcli/task/list_tasks_added_task_history.py b/annofabcli/task/list_tasks_added_task_history.py index e98f22cc..00f4e0b5 100644 --- a/annofabcli/task/list_tasks_added_task_history.py +++ b/annofabcli/task/list_tasks_added_task_history.py @@ -1,176 +1,164 @@ from __future__ import annotations import argparse -import asyncio -import json import logging -import sys -import tempfile from pathlib import Path -from typing import Any, Dict, List, Optional, Set +from typing import Any, List, Optional import annofabapi import more_itertools import pandas -from annofabapi.models import ProjectMemberRole, Task, TaskHistory, TaskPhase, TaskStatus +from annofabapi.models import Task, TaskHistory, TaskPhase, TaskStatus from annofabapi.utils import get_task_history_index_skipped_acceptance, get_task_history_index_skipped_inspection import annofabcli -from annofabcli.common.cli import ( - COMMAND_LINE_ERROR_STATUS_CODE, - AbstractCommandLineInterface, - ArgumentParser, - build_annofabapi_resource_and_login, -) -from annofabcli.common.dataclasses import WaitOptions -from annofabcli.common.download import DownloadingFile +from annofabcli.common.cli import AbstractCommandLineInterface, ArgumentParser, build_annofabapi_resource_and_login from annofabcli.common.enums import FormatArgument -from annofabcli.common.facade import AnnofabApiFacade, TaskQuery, match_task_with_query +from annofabcli.common.facade import AnnofabApiFacade from annofabcli.common.utils import print_csv, print_json from annofabcli.common.visualize import AddProps +from annofabcli.task.list_tasks import ListTasksMain logger = logging.getLogger(__name__) -TaskHistoryDict = Dict[str, List[TaskHistory]] -"""タスク履歴の辞書(key: task_id, value: タスク履歴一覧)""" - -DEFAULT_WAIT_OPTIONS = WaitOptions(interval=60, max_tries=360) - - -def get_completed_datetime(task: dict[str, Any], task_histories: list[TaskHistory]) -> Optional[str]: - """受入完了状態になった日時を取得する。 +class AddingAdditionalInfoToTask: + """タスクに付加的な情報を追加するためのクラス Args: - task_histories (List[TaskHistory]): [description] + service: annofabapiにアクセスするためのインスタンス + project_id: プロジェクトID - Returns: - str: 受入完了状態になった日時 """ - # 受入完了日時を設定 - if task["phase"] == TaskPhase.ACCEPTANCE.value and task["status"] == TaskStatus.COMPLETE.value: - assert len(task_histories) > 0 - return task_histories[-1]["ended_datetime"] - else: - return None - -def get_task_created_datetime(task_histories: list[TaskHistory]) -> Optional[str]: - """タスクの作成日時を取得する。 - - Args: - task_histories (List[TaskHistory]): タスク履歴 - - Returns: - タスクの作成日時 - """ - # 受入フェーズで完了日時がnot Noneの場合は、受入を合格したか差し戻したとき。 - # したがって、後続のタスク履歴を見て、初めて受入完了状態になった日時を取得する。 - if len(task_histories) == 0: - return None + def __init__(self, service: annofabapi.Resource, project_id: str): + self.service = service + self.project_id = project_id + self.visualize = AddProps(self.service, project_id) - first_history = task_histories[0] - # 2020年以前は、先頭のタスク履歴はタスク作成ではなく、教師付けの履歴である。2020年以前はタスク作成日時を取得できないのでNoneを返す。 - # https://annofab.com/docs/releases/2020.html#v01020 - if ( - first_history["account_id"] is None - and first_history["accumulated_labor_time_milliseconds"] == "PT0S" - and first_history["phase"] == TaskPhase.ANNOTATION.value - ): - return first_history["started_datetime"] - return None + @staticmethod + def get_completed_datetime(task: dict[str, Any], task_histories: list[TaskHistory]) -> Optional[str]: + """受入完了状態になった日時を取得する。 + Args: + task_histories (List[TaskHistory]): [description] -def get_first_acceptance_completed_datetime(task_histories: list[TaskHistory]) -> Optional[str]: - """はじめて受入完了状態になった日時を取得する。 + Returns: + str: 受入完了状態になった日時 + """ + # 受入完了日時を設定 + if task["phase"] == TaskPhase.ACCEPTANCE.value and task["status"] == TaskStatus.COMPLETE.value: + assert len(task_histories) > 0 + return task_histories[-1]["ended_datetime"] + else: + return None - Args: - task_histories (List[TaskHistory]): [description] + @staticmethod + def get_task_created_datetime(task: dict[str, Any], task_histories: list[TaskHistory]) -> Optional[str]: + """タスクの作成日時を取得する。 - Returns: - str: はじめて受入完了状態になった日時 - """ - # 受入フェーズで完了日時がnot Noneの場合は、受入を合格したか差し戻したとき。 - # したがって、後続のタスク履歴を見て、初めて受入完了状態になった日時を取得する。 + Args: + task_histories (List[TaskHistory]): タスク履歴 - for index, history in enumerate(task_histories): - if history["phase"] != TaskPhase.ACCEPTANCE.value or history["ended_datetime"] is None: - continue + Returns: + タスクの作成日時 + """ + # 受入フェーズで完了日時がnot Noneの場合は、受入を合格したか差し戻したとき。 + # したがって、後続のタスク履歴を見て、初めて受入完了状態になった日時を取得する。 + if len(task_histories) == 0: + return None + + first_history = task_histories[0] + # 2020年以前は、先頭のタスク履歴はタスク作成ではなく、教師付けの履歴である。2020年以前はタスク作成日時を取得できないのでNoneを返す。 + # https://annofab.com/docs/releases/2020.html#v01020 + if ( + first_history["account_id"] is None + and first_history["accumulated_labor_time_milliseconds"] == "PT0S" + and first_history["phase"] == TaskPhase.ANNOTATION.value + ): + if len(task_histories) == 1: + # 一度も作業されていないタスクは、先頭のタスク履歴のstarted_datetimeはNoneである + # 替わりにタスクの`operation_updated_datetime`をタスク作成日時とする + return task["operation_updated_datetime"] + return first_history["started_datetime"] + return None - if index == len(task_histories) - 1: - # 末尾履歴なら、受入完了状態 - return history["ended_datetime"] + @staticmethod + def get_first_acceptance_completed_datetime(task_histories: list[TaskHistory]) -> Optional[str]: + """はじめて受入完了状態になった日時を取得する。 - next_history = task_histories[index + 1] - if next_history["phase"] == TaskPhase.ACCEPTANCE.value: - # 受入完了後、受入取り消し実行 - return history["ended_datetime"] - # そうでなければ、受入フェーズでの差し戻し + Args: + task_histories (List[TaskHistory]): [description] - return None + Returns: + str: はじめて受入完了状態になった日時 + """ + # 受入フェーズで完了日時がnot Noneの場合は、受入を合格したか差し戻したとき。 + # したがって、後続のタスク履歴を見て、初めて受入完了状態になった日時を取得する。 + for index, history in enumerate(task_histories): + if history["phase"] != TaskPhase.ACCEPTANCE.value or history["ended_datetime"] is None: + continue -def is_acceptance_phase_skipped(task_histories: list[TaskHistory]) -> bool: - """抜取受入によって、受入フェーズでスキップされたことがあるかを取得する。 + if index == len(task_histories) - 1: + # 末尾履歴なら、受入完了状態 + return history["ended_datetime"] - Args: - task_histories (List[TaskHistory]): タスク履歴 + next_history = task_histories[index + 1] + if next_history["phase"] == TaskPhase.ACCEPTANCE.value: + # 受入完了後、受入取り消し実行 + return history["ended_datetime"] + # そうでなければ、受入フェーズでの差し戻し - Returns: - bool: 受入フェーズでスキップされたことがあるかどうか - """ - task_history_index_list = get_task_history_index_skipped_acceptance(task_histories) - if len(task_history_index_list) == 0: - return False - - # スキップされた履歴より後に受入フェーズがなければ、受入がスキップされたタスクとみなす - # ただし、スキップされた履歴より後で、「アノテーション一覧で修正された」受入フェーズがある場合(account_id is None)は、スキップされた受入とみなす。 - last_task_history_index = task_history_index_list[-1] - return ( - more_itertools.first_true( - task_histories[last_task_history_index + 1 :], - pred=lambda e: e["phase"] == TaskPhase.ACCEPTANCE.value and e["account_id"] is not None, - ) - is None - ) + return None + @staticmethod + def is_acceptance_phase_skipped(task_histories: list[TaskHistory]) -> bool: + """抜取受入によって、受入フェーズでスキップされたことがあるかを取得する。 -def is_inspection_phase_skipped(task_histories: list[TaskHistory]) -> bool: - """抜取検査によって、検査フェーズでスキップされたことがあるかを取得する。 + Args: + task_histories (List[TaskHistory]): タスク履歴 - Args: - task_histories (List[TaskHistory]): タスク履歴 + Returns: + bool: 受入フェーズでスキップされたことがあるかどうか + """ + task_history_index_list = get_task_history_index_skipped_acceptance(task_histories) + if len(task_history_index_list) == 0: + return False - Returns: - bool: 検査フェーズでスキップされたことがあるかどうか - """ - task_history_index_list = get_task_history_index_skipped_inspection(task_histories) - if len(task_history_index_list) == 0: - return False - - # スキップされた履歴より後に検査フェーズがなければ、検査がスキップされたタスクとみなす - last_task_history_index = task_history_index_list[-1] - return ( - more_itertools.first_true( - task_histories[last_task_history_index + 1 :], pred=lambda e: e["phase"] == TaskPhase.INSPECTION.value + # スキップされた履歴より後に受入フェーズがなければ、受入がスキップされたタスクとみなす + # ただし、スキップされた履歴より後で、「アノテーション一覧で修正された」受入フェーズがある場合(account_id is None)は、スキップされた受入とみなす。 + last_task_history_index = task_history_index_list[-1] + return ( + more_itertools.first_true( + task_histories[last_task_history_index + 1 :], + pred=lambda e: e["phase"] == TaskPhase.ACCEPTANCE.value and e["account_id"] is not None, + ) + is None ) - is None - ) + @staticmethod + def is_inspection_phase_skipped(task_histories: list[TaskHistory]) -> bool: + """抜取検査によって、検査フェーズでスキップされたことがあるかを取得する。 -class AddingAdditionalInfoToTask: - """タスクに付加的な情報を追加するためのクラス - - Args: - service: annofabapiにアクセスするためのインスタンス - project_id: プロジェクトID + Args: + task_histories (List[TaskHistory]): タスク履歴 - """ + Returns: + bool: 検査フェーズでスキップされたことがあるかどうか + """ + task_history_index_list = get_task_history_index_skipped_inspection(task_histories) + if len(task_history_index_list) == 0: + return False - def __init__(self, service: annofabapi.Resource, project_id: str): - self.service = service - self.project_id = project_id - self.visualize = AddProps(self.service, project_id) + # スキップされた履歴より後に検査フェーズがなければ、検査がスキップされたタスクとみなす + last_task_history_index = task_history_index_list[-1] + return ( + more_itertools.first_true( + task_histories[last_task_history_index + 1 :], pred=lambda e: e["phase"] == TaskPhase.INSPECTION.value + ) + is None + ) def _add_task_history_info(self, task: Task, task_history: Optional[TaskHistory], column_prefix: str) -> Task: """ @@ -281,7 +269,7 @@ def add_task_history_additional_info_to_task(self, task: dict[str, Any], task_hi """ # タスク作成日時 - task["created_datetime"] = get_task_created_datetime(task_histories) + task["created_datetime"] = self.get_task_created_datetime(task, task_histories) # フェーズごとのタスク履歴情報を追加する self._add_task_history_info_by_phase(task, task_histories, phase=TaskPhase.ANNOTATION) @@ -289,42 +277,50 @@ def add_task_history_additional_info_to_task(self, task: dict[str, Any], task_hi self._add_task_history_info_by_phase(task, task_histories, phase=TaskPhase.ACCEPTANCE) # 初めて受入が完了した日時 - task["first_acceptance_completed_datetime"] = get_first_acceptance_completed_datetime(task_histories) + task["first_acceptance_completed_datetime"] = self.get_first_acceptance_completed_datetime(task_histories) # 受入完了日時を設定 - task["completed_datetime"] = get_completed_datetime(task, task_histories) + task["completed_datetime"] = self.get_completed_datetime(task, task_histories) # 抜取検査/受入によって、スキップされたかどうか - task["inspection_is_skipped"] = is_inspection_phase_skipped(task_histories) - task["acceptance_is_skipped"] = is_acceptance_phase_skipped(task_histories) + task["inspection_is_skipped"] = self.is_inspection_phase_skipped(task_histories) + task["acceptance_is_skipped"] = self.is_acceptance_phase_skipped(task_histories) -class ListTasksAddedTaskHistory(AbstractCommandLineInterface): - """ - タスクの一覧を表示する - """ +class ListTasksAddedTaskHistoryMain: + def __init__(self, service: annofabapi.Resource, project_id: str) -> None: + self.service = service + self.project_id = project_id - def get_detail_task_list( - self, - task_list: List[Dict[str, Any]], - task_history_dict: TaskHistoryDict, - project_id: str, - ): - obj = AddingAdditionalInfoToTask(self.service, project_id=project_id) + def main(self, *, task_query: Optional[dict[str, Any]], task_id_list: Optional[list[str]]): + list_task_obj = ListTasksMain(self.service, self.project_id) + task_list = list_task_obj.get_task_list(self.project_id, task_id_list=task_id_list, task_query=task_query) - for task in task_list: + obj = AddingAdditionalInfoToTask(self.service, project_id=self.project_id) - obj.add_additional_info_to_task(task) + for index, task in enumerate(task_list): + if (index + 1) % 1000 == 0: + logger.debug(f"{index+1} 件目のタスク履歴情報を取得します。") + obj.add_additional_info_to_task(task) task_id = task["task_id"] - task_histories = task_history_dict.get(task_id) - if task_histories is None: - logger.warning(f"task_id='{task_id}' に紐づくタスク履歴情報は存在しないので、タスク履歴の付加的情報はタスクに追加しません。") - continue - obj.add_task_history_additional_info_to_task(task, task_histories) + + try: + task_histories, _ = self.service.api.get_task_histories(self.project_id, task_id) + # タスク履歴から取得した情報をtaskに設定する + obj.add_task_history_additional_info_to_task(task, task_histories) + except Exception: + logger.warning(f"task_id='{task_id}' :: タスク履歴に関する情報を取得するのに失敗しました。", exc_info=True) return task_list + +class TasksAddedTaskHistoryOutput: + """出力用のクラス""" + + def __init__(self, task_list: list[dict[str, Any]]): + self.task_list = task_list + @staticmethod def _get_output_target_columns() -> List[str]: base_columns = [ @@ -365,148 +361,33 @@ def _get_output_target_columns() -> List[str]: return base_columns + task_history_columns - def download_json_files( - self, - project_id: str, - task_json_path: Path, - task_history_json_path: Path, - is_latest: bool, - ): - loop = asyncio.get_event_loop() - downloading_obj = DownloadingFile(self.service) - gather = asyncio.gather( - downloading_obj.download_task_json_with_async( - project_id, dest_path=str(task_json_path), is_latest=is_latest - ), - downloading_obj.download_task_history_json_with_async( - project_id, - dest_path=str(task_history_json_path), - ), - ) - loop.run_until_complete(gather) - - @staticmethod - def validate(args: argparse.Namespace) -> bool: - COMMON_MESSAGE = "annofabcli task list_merged_task_history: error:" - if (args.task_json is None and args.task_history_json is not None) or ( - args.task_json is not None and args.task_history_json is None - ): - print( - f"{COMMON_MESSAGE} '--task_json'と'--task_history_json'の両方を指定する必要があります。", - file=sys.stderr, - ) - return False - - return True - - @staticmethod - def match_task_with_conditions( - task: Dict[str, Any], - task_id_set: Optional[Set[str]] = None, - task_query: Optional[TaskQuery] = None, - ) -> bool: - result = True - - dc_task = annofabapi.dataclass.task.Task.from_dict(task) - result = result and match_task_with_query(dc_task, task_query) - if task_id_set is not None: - result = result and (dc_task.task_id in task_id_set) - return result - - def filter_task_list( - self, - project_id: str, - task_list: List[Dict[str, Any]], - task_id_list: Optional[List[str]] = None, - task_query: Optional[TaskQuery] = None, - ) -> List[Dict[str, Any]]: - if task_query is not None: - task_query = self.facade.set_account_id_of_task_query(project_id, task_query) - - task_id_set = set(task_id_list) if task_id_list is not None else None - logger.debug(f"出力対象のタスクを抽出しています。") - filtered_task_list = [ - e for e in task_list if self.match_task_with_conditions(e, task_query=task_query, task_id_set=task_id_set) - ] - return filtered_task_list - - def print_task_list( - self, - project_id: str, - task_json_path: Optional[Path], - task_history_json_path: Optional[Path], - is_latest: bool, - task_id_list: Optional[list[str]], - task_query: Optional[TaskQuery], - arg_format: FormatArgument, - output: Path, - ): - super().validate_project(project_id, [ProjectMemberRole.OWNER, ProjectMemberRole.TRAINING_DATA_USER]) - - downloading_obj = DownloadingFile(self.service) - - if task_json_path is not None: - with task_json_path.open(encoding="utf-8") as f: - task_list = json.load(f) - else: - with tempfile.NamedTemporaryFile() as tmp_file: - downloading_obj.download_task_json(project_id, tmp_file.name, is_latest=is_latest) - with open(tmp_file.name, encoding="utf-8") as f: - task_list = json.load(f) - - if task_history_json_path is not None: - with task_history_json_path.open(encoding="utf-8") as f: - task_history_dict = json.load(f) - else: - with tempfile.NamedTemporaryFile() as tmp_file: - downloading_obj.download_task_history_json(project_id, tmp_file.name) - with open(tmp_file.name, encoding="utf-8") as f: - task_history_dict = json.load(f) - - filtered_task_list = self.filter_task_list( - project_id, task_list, task_id_list=task_id_list, task_query=task_query - ) - - logger.debug(f"タスク履歴に関する付加的情報を取得しています。") - detail_task_list = self.get_detail_task_list( - project_id=project_id, task_list=filtered_task_list, task_history_dict=task_history_dict - ) - - if arg_format == FormatArgument.CSV: - df_task = pandas.DataFrame(detail_task_list) + def output(self, output_path: Path, output_format: FormatArgument): + task_list = self.task_list + logger.debug(f"タスク一覧の件数: {len(task_list)}") + if output_format == FormatArgument.CSV: + df_task = pandas.DataFrame(task_list) print_csv( df_task[self._get_output_target_columns()], - output=self.output, + output=output_path, ) - elif arg_format == FormatArgument.JSON: - print_json(detail_task_list, is_pretty=False, output=output) - elif arg_format == FormatArgument.PRETTY_JSON: - print_json(detail_task_list, is_pretty=True, output=output) + elif output_format == FormatArgument.JSON: + print_json(task_list, is_pretty=False, output=output_path) + elif output_format == FormatArgument.PRETTY_JSON: + print_json(task_list, is_pretty=True, output=output_path) + +class ListTasksAddedTaskHistory(AbstractCommandLineInterface): def main(self): args = self.args - if not self.validate(args): - sys.exit(COMMAND_LINE_ERROR_STATUS_CODE) - - project_id = args.project_id task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id) if args.task_id is not None else None - task_query = ( - TaskQuery.from_dict(annofabcli.common.cli.get_json_from_args(args.task_query)) - if args.task_query is not None - else None - ) + task_query = annofabcli.common.cli.get_json_from_args(args.task_query) if args.task_query is not None else None - self.print_task_list( - project_id, - task_json_path=args.task_json, - task_history_json_path=args.task_history_json, - is_latest=args.latest, - task_id_list=task_id_list, - task_query=task_query, - arg_format=FormatArgument(args.format), - output=args.output, - ) + main_obj = ListTasksAddedTaskHistoryMain(self.service, project_id=args.project_id) + task_list = main_obj.main(task_query=task_query, task_id_list=task_id_list) + + output_obj = TasksAddedTaskHistoryOutput(task_list) + output_obj.output(args.output, output_format=FormatArgument(args.format)) def main(args): @@ -518,27 +399,28 @@ def main(args): def parse_args(parser: argparse.ArgumentParser): argument_parser = ArgumentParser(parser) argument_parser.add_project_id() - argument_parser.add_task_query() - argument_parser.add_task_id(required=False) - parser.add_argument( - "--task_json", - type=str, - help="タスク情報が記載されたJSONファイルのパスを指定すると、JSONに記載された情報を元に出力します。指定しない場合はJSONファイルをダウンロードします。\n" - "JSONファイルは ``$ annofabcli task download`` コマンドで取得できます。", - ) + query_group = parser.add_mutually_exclusive_group() - parser.add_argument( - "--task_history_json", + # タスク検索クエリ + query_group.add_argument( + "-tq", + "--task_query", type=str, - help="タスク履歴情報が記載されたJSONファイルのパスを指定すると、JSONに記載された情報を元に出力します。指定しない場合はJSONファイルをダウンロードします。\n" - "JSONファイルは ``$ annofabcli task_history download`` コマンドで取得できます。", + help="タスクの検索クエリをJSON形式で指定します。指定しない場合は、すべてのタスクを取得します。" + " ``file://`` を先頭に付けると、JSON形式のファイルを指定できます。" + "クエリのフォーマットは、`getTasks `_ APIのクエリパラメータと同じです。" + "さらに追加で、``user_id`` , ``previous_user_id`` キーも指定できます。" + "ただし ``page`` , ``limit`` キーは指定できません。", ) - parser.add_argument( - "--latest", - action="store_true", - help="タスク一覧ファイルの更新が完了するまで待って、最新のファイルをダウンロードします(タスク履歴ファイルはWebAPIの都合上更新されません)。" "JSONファイルを指定しなかったときに有効です。", + query_group.add_argument( + "-t", + "--task_id", + type=str, + nargs="+", + help="対象のタスクのtask_idを指定します。 ``--task_query`` 引数とは同時に指定できません。" + " ``file://`` を先頭に付けると、task_idの一覧が記載されたファイルを指定できます。", ) argument_parser.add_output() @@ -553,9 +435,8 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: Optional[argparse._SubParsersAction] = None): subcommand_name = "list_added_task_history" - subcommand_help = "タスク履歴情報を加えたタスク一覧を出力します。" - description = "タスク履歴情報(フェーズごとの作業時間、担当者、開始日時)を加えたタスク一覧を出力します。" - epilog = "アノテーションユーザ/オーナロールを持つユーザで実行してください。" - parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog) + subcommand_help = "タスク履歴に関する情報を加えたタスク一覧を出力します。" + description = "タスク履歴に関する情報(フェーズごとの作業時間、担当者、開始日時)を加えたタスク一覧を出力します。" + parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description) parse_args(parser) return parser diff --git a/annofabcli/task/subcommand_task.py b/annofabcli/task/subcommand_task.py index c8fe398e..50c48d45 100644 --- a/annofabcli/task/subcommand_task.py +++ b/annofabcli/task/subcommand_task.py @@ -11,6 +11,7 @@ import annofabcli.task.delete_tasks import annofabcli.task.download_task_json import annofabcli.task.list_all_tasks +import annofabcli.task.list_all_tasks_added_task_history import annofabcli.task.list_tasks import annofabcli.task.list_tasks_added_task_history import annofabcli.task.put_tasks @@ -32,8 +33,9 @@ def parse_args(parser: argparse.ArgumentParser): annofabcli.task.delete_tasks.add_parser(subparsers) annofabcli.task.download_task_json.add_parser(subparsers) annofabcli.task.list_tasks.add_parser(subparsers) - annofabcli.task.list_all_tasks.add_parser(subparsers) annofabcli.task.list_tasks_added_task_history.add_parser(subparsers) + annofabcli.task.list_all_tasks.add_parser(subparsers) + annofabcli.task.list_all_tasks_added_task_history.add_parser(subparsers) annofabcli.task.put_tasks.add_parser(subparsers) annofabcli.task.put_tasks_by_count.add_parser(subparsers) annofabcli.task.reject_tasks.add_parser(subparsers) diff --git a/docs/command_reference/task/index.rst b/docs/command_reference/task/index.rst index d127460d..38d439f6 100644 --- a/docs/command_reference/task/index.rst +++ b/docs/command_reference/task/index.rst @@ -23,8 +23,9 @@ Available Commands delete download list - list_all list_added_task_history + list_all + list_all_added_task_history put put_by_count reject diff --git a/docs/command_reference/task/list_added_task_history.rst b/docs/command_reference/task/list_added_task_history.rst index 57a0a2da..570db835 100644 --- a/docs/command_reference/task/list_added_task_history.rst +++ b/docs/command_reference/task/list_added_task_history.rst @@ -4,13 +4,14 @@ task list_added_task_history Description ================================= -`annofabcli task list <../task/list.html>`_ コマンドで取得できるタスク一覧に、タスク履歴から取得した以下の情報を追加した上で出力します。 +タスク一覧に、タスク履歴に関する情報に加えたものを出力します。 +タスク履歴に関する情報は、たとえば以下のような情報です。 * フェーズごとの作業時間 * 各フェーズの最初の担当者と開始日時 * 各フェーズの最後の担当者と開始日時 -最初に教師付けを開始した日時や担当者などを調べるのに利用できます。 +最初に教師付けを開始した日時や担当者などを調べるのに、便利です。 Examples @@ -20,27 +21,26 @@ Examples 基本的な使い方 -------------------------- -以下のコマンドは、タスク全件ファイルとタスク履歴全件ファイルをダウンロードしてから、タスク一覧を出力します。 - .. code-block:: $ annofabcli task list_added_task_history --project_id prj1 --output task.csv -タスク全件ファイルを指定する場合は ``--task_json`` 、タスク履歴全件ファイルを指定する場合は ``--task_history_json`` を指定してください。 - -.. code-block:: +.. warning:: - $ annofabcli task list_added_task_history --project_id prj1 --output task.csv \ - --task_json task.json --task_history_json task_history.json + WebAPIの都合上、タスクは10,000件までしか出力できません。 + 10,000件以上のタスクを出力する場合は、`annofabcli task list_all_added_task_history <../task/list_all_added_task_history.html>`_ コマンドを使用してください。 -タスク全件ファイルは `annofabcli task download <../task/download.html>`_ コマンド、タスク履歴全件ファイルは、`annofabcli task_history download <../task_history/download.html>`_ コマンドでダウンロードできます。 -タスクの絞り込み +タスクのフェーズやステータスなどで絞り込む ---------------------------------------------- +``--task_query`` を指定すると、タスクのフェーズやステータスなどで絞り込めます。 + +``--task_query`` に渡す値は、https://annofab.com/docs/api/#operation/getTasks のクエリパラメータとほとんど同じです。 +さらに追加で、``user_id`` , ``previous_user_id`` キーも指定できます。 -``--task_query`` 、 ``--task_id`` で、タスクを絞り込むことができます。 +以下のコマンドは、受入フェーズで完了状態のタスク一覧を出力します。 .. code-block:: @@ -48,10 +48,16 @@ Examples $ annofabcli task list_added_task_history --project_id prj1 \ --task_query '{"status":"complete", "phase":"not_started"}' - $ annofabcli task list_added_task_history --project_id prj1 \ - --task_id file://task_id.txt +task_id絞り込む +-------------------------------------------------------------------------------------------- +``--task_id`` を指定すると、タスクIDで絞り込むことができます。 + +.. code-block:: + + $ annofabcli task list_added_task_history --project_id prj1 \ + --task_id task1 task2 @@ -147,16 +153,6 @@ JSON出力 -CSV出力 ----------------------------------------------- - -.. code-block:: - - $ annofabcli task list_added_task_history --project_id prj1 --output out.csv - - -`out.csv `_ - Usage Details diff --git a/docs/command_reference/task/list_added_task_history/out.csv b/docs/command_reference/task/list_added_task_history/out.csv deleted file mode 100644 index 153c6a76..00000000 --- a/docs/command_reference/task/list_added_task_history/out.csv +++ /dev/null @@ -1,3 +0,0 @@ -task_id,phase,phase_stage,status,started_datetime,updated_datetime,user_id,username,worktime_hour,annotation_worktime_hour,inspection_worktime_hour,acceptance_worktime_hour,number_of_rejections_by_inspection,number_of_rejections_by_acceptance,first_acceptance_completed_datetime,completed_datetime,inspection_is_skipped,acceptance_is_skipped,first_annotation_user_id,first_annotation_username,first_annotation_started_datetime,first_inspection_user_id,first_inspection_username,first_inspection_started_datetime,first_acceptance_user_id,first_acceptance_username,first_acceptance_started_datetime,last_annotation_user_id,last_annotation_username,last_annotation_started_datetime,last_inspection_user_id,last_inspection_username,last_inspection_started_datetime,last_acceptance_user_id,last_acceptance_username,last_acceptance_started_datetime -task1,annotation,1,not_started,,2020-12-09T16:18:08.12+09:00,test_user_id,test_username,0.007230277777778,0.007230277777778,0,0,0,0,2022-01-25T10:55:25.996+09:00,2022-02-02T01:35:58.654+09:00,False,False,,,2020-02-18T11:13:33.961+09:00,,,2020-12-08T21:42:23.115+09:00,,,,test_user_id,test_username,,,,2020-12-09T16:18:08.116+09:00,,, -task2,annotation,1,break,2020-05-15T14:54:12.067+09:00,2020-09-09T15:22:51.105+09:00,test_user_id,test_username,1.74646416666667,1.74646416666667,0,0,0,0,2022-02-07T18:36:31.716+09:00,2022-02-07T18:36:31.716+09:00,False,False,test_user_id,test_username,2020-05-15T14:54:12.067+09:00,,,,,,,test_user_id,test_username,2020-05-15T14:54:12.067+09:00,,,,,, diff --git a/docs/command_reference/task/list_all_added_task_history.rst b/docs/command_reference/task/list_all_added_task_history.rst new file mode 100644 index 00000000..2441ce16 --- /dev/null +++ b/docs/command_reference/task/list_all_added_task_history.rst @@ -0,0 +1,71 @@ +========================================== +task list_all_added_task_history +========================================== + +Description +================================= +すべてのタスク一覧に、タスク履歴に関する情報に加えたものを出力します。 +出力内容は `annofabcli task list_added_task_history <../task/list_added_task_history.html>`_ コマンドと同じです。 + +.. note:: + + 出力されるタスクは、コマンドを実行した日の02:00(JST)頃の状態です。 + + + +Examples +================================= + + +基本的な使い方 +-------------------------- + +以下のコマンドは、タスク全件ファイルとタスク履歴全件ファイルをダウンロードしてから、タスク一覧を出力します。 + +.. code-block:: + + $ annofabcli task list_all_added_task_history --project_id prj1 --output task.csv + + +タスク全件ファイルを指定する場合は ``--task_json`` 、タスク履歴全件ファイルを指定する場合は ``--task_history_json`` を指定してください。 + +.. code-block:: + + $ annofabcli task list_all_added_task_history --project_id prj1 --output task.csv \ + --task_json task.json --task_history_json task_history.json + +タスク全件ファイルは `annofabcli task download <../task/download.html>`_ コマンド、タスク履歴全件ファイルは、`annofabcli task_history download <../task_history/download.html>`_ コマンドでダウンロードできます。 + + +タスクの絞り込み +---------------------------------------------- + +``--task_query`` 、 ``--task_id`` で、タスクを絞り込むことができます。 + + +.. code-block:: + + $ annofabcli task list_all_added_task_history --project_id prj1 \ + --task_query '{"status":"complete", "phase":"not_started"}' + + $ annofabcli task list_all_added_task_history --project_id prj1 \ + --task_id file://task_id.txt + + + + + +出力結果 +================================= + +出力内容は `annofabcli task list_added_task_history <../task/list_added_task_history.html>`_ コマンドと同じです。 + + +Usage Details +================================= + +.. argparse:: + :ref: annofabcli.task.list_all_tasks_added_task_history.add_parser + :prog: annofabcli task list_all_added_task_history + :nosubcommands: + :nodefaultconst: diff --git a/pyproject.toml b/pyproject.toml index 10441347..8b507e03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "annofabcli" -version = "1.73.3" +version = "1.74.0" description = "Utility Command Line Interface for AnnoFab" authors = ["yuji38kwmt"] license = "MIT" diff --git a/tests/test_task.py b/tests/test_task.py index 4dd24f8f..4dfbcdeb 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -54,6 +54,21 @@ def test_list(self): ] ) + def test_list_all_added_task_history(self): + out_file = str(out_dir / "list_all_added_task_history.csv") + main( + [ + self.command_name, + "list_all_added_task_history", + "--project_id", + project_id, + "--task_id", + task_id, + "--output", + out_file, + ] + ) + def test_list_added_task_history(self): out_file = str(out_dir / "list_added_task_history.csv") main( @@ -62,6 +77,8 @@ def test_list_added_task_history(self): "list_added_task_history", "--project_id", project_id, + "--task_id", + task_id, "--output", out_file, ]