From c91b3693da1cf91765a4ad026d90e67f3179a67b Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Tue, 24 Dec 2019 18:10:44 +0900 Subject: [PATCH 01/19] =?UTF-8?q?=E3=82=B0=E3=83=A9=E3=83=95=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- annofabcli/statistics/database.py | 60 +- annofabcli/statistics/graph.py | 710 ++++++++++++++---- .../statistics/subcommand_statistics.py | 2 +- annofabcli/statistics/table.py | 114 +-- annofabcli/statistics/tsv.py | 13 +- annofabcli/statistics/visualize_statistics.py | 130 +++- 6 files changed, 761 insertions(+), 268 deletions(-) diff --git a/annofabcli/statistics/database.py b/annofabcli/statistics/database.py index 72d3cf42..5aaafc4f 100644 --- a/annofabcli/statistics/database.py +++ b/annofabcli/statistics/database.py @@ -120,7 +120,7 @@ def read_annotation_summary(task_id: str, input_data_id_: str) -> Dict[str, Any] simple_annotation = parser.parse() return annotation_summary_func(simple_annotation.details) - with zipfile.ZipFile(self.annotations_zip_path, 'r') as zip_file: + with zipfile.ZipFile(self.annotations_zip_path, "r") as zip_file: annotation_dict: AnnotationDict = {} task_count = 0 @@ -169,9 +169,12 @@ def wait_for_completion_updated_annotation(self, project_id): MAX_JOB_ACCESS = 120 JOB_ACCESS_INTERVAL = 60 MAX_WAIT_MINUTU = MAX_JOB_ACCESS * JOB_ACCESS_INTERVAL / 60 - result = self.annofab_service.wrapper.wait_for_completion(project_id, job_type=JobType.GEN_ANNOTATION, - job_access_interval=JOB_ACCESS_INTERVAL, - max_job_access=MAX_JOB_ACCESS) + result = self.annofab_service.wrapper.wait_for_completion( + project_id, + job_type=JobType.GEN_ANNOTATION, + job_access_interval=JOB_ACCESS_INTERVAL, + max_job_access=MAX_JOB_ACCESS, + ) if result: logger.info(f"アノテーションの更新が完了しました。") else: @@ -182,9 +185,12 @@ def wait_for_completion_updated_task_json(self, project_id): MAX_JOB_ACCESS = 120 JOB_ACCESS_INTERVAL = 60 MAX_WAIT_MINUTU = MAX_JOB_ACCESS * JOB_ACCESS_INTERVAL / 60 - result = self.annofab_service.wrapper.wait_for_completion(project_id, job_type=JobType.GEN_TASKS_LIST, - job_access_interval=JOB_ACCESS_INTERVAL, - max_job_access=MAX_JOB_ACCESS) + result = self.annofab_service.wrapper.wait_for_completion( + project_id, + job_type=JobType.GEN_TASKS_LIST, + job_access_interval=JOB_ACCESS_INTERVAL, + max_job_access=MAX_JOB_ACCESS, + ) if result: logger.info(f"タスク全件ファイルの更新が完了しました。") else: @@ -204,7 +210,7 @@ def update_annotation_zip(self, project_id: str, should_update_annotation_zip: b """ project, _ = self.annofab_service.api.get_project(project_id) - last_tasks_updated_datetime = project['summary']['last_tasks_updated_datetime'] + last_tasks_updated_datetime = project["summary"]["last_tasks_updated_datetime"] logger.debug(f"タスクの最終更新日時={last_tasks_updated_datetime}") annotation_specs_history = self.annofab_service.api.get_annotation_specs_histories(project_id)[0] @@ -226,20 +232,20 @@ def update_annotation_zip(self, project_id: str, should_update_annotation_zip: b self.wait_for_completion_updated_annotation(project_id) else: job = job_list[0] - job_status = JobStatus(job['job_status']) + job_status = JobStatus(job["job_status"]) if job_status == JobStatus.PROGRESS: logger.info(f"アノテーション更新が完了するまで待ちます。") self.wait_for_completion_updated_annotation(project_id) elif job_status == JobStatus.SUCCEEDED: if dateutil.parser.parse( - job['updated_datetime']) < dateutil.parser.parse(last_tasks_updated_datetime): + job["updated_datetime"]) < dateutil.parser.parse(last_tasks_updated_datetime): logger.info(f"タスクの最新更新日時よりアノテーションzipの最終更新日時の方が古いので、アノテーションzipを更新します。") self.annofab_service.api.post_annotation_archive_update(project_id) self.wait_for_completion_updated_annotation(project_id) elif dateutil.parser.parse( - job['updated_datetime']) < dateutil.parser.parse(annotation_specs_updated_datetime): + job["updated_datetime"]) < dateutil.parser.parse(annotation_specs_updated_datetime): logger.info(f"アノテーション仕様の更新日時よりアノテーションzipの最終更新日時の方が古いので、アノテーションzipを更新します。") self.annofab_service.api.post_annotation_archive_update(project_id) self.wait_for_completion_updated_annotation(project_id) @@ -277,7 +283,7 @@ def update_task_json(self, project_id: str, should_update_task_json: bool = Fals logger.debug(f"タスク全件ファイルの最終更新日時={job['updated_datetime']}, job_status={job['job_status']}") if should_update_task_json: - job_status = JobStatus(job['job_status']) + job_status = JobStatus(job["job_status"]) if job_status == JobStatus.PROGRESS: logger.info(f"タスク全件ファイルの更新が完了するまで待ちます。") self.wait_for_completion_updated_task_json(project_id) @@ -291,7 +297,11 @@ def update_task_json(self, project_id: str, should_update_task_json: bool = Fals self.annofab_service.api.post_project_tasks_update(project_id) self.wait_for_completion_updated_task_json(project_id) - def _download_db_file(self, should_update_annotation_zip: bool = False, should_update_task_json: bool = False): + def _download_db_file( + self, + should_update_annotation_zip: bool = False, + should_update_task_json: bool = False, + ): """ DBになりうるファイルをダウンロードする @@ -315,8 +325,11 @@ def _download_db_file(self, should_update_annotation_zip: bool = False, should_u self.annofab_service.wrapper.download_annotation_archive(self.project_id, annotations_zip_file, v2=True) # task historiesは未完成なので、使わない - def read_tasks_from_json(self, task_query_param: Optional[Dict[str, Any]] = None, - ignored_task_id_list: Optional[List[str]] = None) -> List[Task]: + def read_tasks_from_json( + self, + task_query_param: Optional[Dict[str, Any]] = None, + ignored_task_id_list: Optional[List[str]] = None, + ) -> List[Task]: """ tass.jsonからqueryに合致するタスクを調べる Args: @@ -350,8 +363,13 @@ def filter_task(arg_task): logger.debug(f"集計対象のタスク数 = {len(filtered_task_list)}") return filtered_task_list - def update_db(self, task_query_param: Dict[str, Any], ignored_task_ids: Optional[List[str]] = None, - should_update_annotation_zip: bool = False, should_update_task_json: bool = False) -> None: + def update_db( + self, + task_query_param: Dict[str, Any], + ignored_task_ids: Optional[List[str]] = None, + should_update_annotation_zip: bool = False, + should_update_task_json: bool = False, + ) -> None: """ Annofabから情報を取得し、DB(pickelファイル)を更新する。 TODO タスク履歴一括ファイルに必要な情報が含まれたら、修正する @@ -366,8 +384,10 @@ def update_db(self, task_query_param: Dict[str, Any], ignored_task_ids: Optional self.filename_timestamp = "{0:%Y%m%d-%H%M%S}".format(datetime.datetime.now()) # DB用のJSONファイルをダウンロードする - self._download_db_file(should_update_annotation_zip=should_update_annotation_zip, - should_update_task_json=should_update_task_json) + self._download_db_file( + should_update_annotation_zip=should_update_annotation_zip, + should_update_task_json=should_update_task_json, + ) logger.info(f"DB更新: task_query_param = {task_query_param}") tasks = self.read_tasks_from_json(task_query_param) @@ -375,7 +395,7 @@ def update_db(self, task_query_param: Dict[str, Any], ignored_task_ids: Optional if ignored_task_ids is not None: # 無視するtask_idを除外する logger.info(f"除外するtask_ids = {ignored_task_ids}") - tasks = [e for e in tasks if e['task_id'] not in ignored_task_ids] + tasks = [e for e in tasks if e["task_id"] not in ignored_task_ids] old_tasks = self.read_tasks_from_checkpoint() not_updated_task_ids = self.get_not_updated_task_ids(old_tasks, tasks) diff --git a/annofabcli/statistics/graph.py b/annofabcli/statistics/graph.py index a2351de4..c045172c 100644 --- a/annofabcli/statistics/graph.py +++ b/annofabcli/statistics/graph.py @@ -1,4 +1,5 @@ import logging +from dataclasses import dataclass from pathlib import Path from typing import Any, Dict, List, Optional # pylint: disable=unused-import @@ -14,7 +15,23 @@ logger = logging.getLogger(__name__) -hv.extension('bokeh') +hv.extension("bokeh") + + +@dataclass +class HistogramName: + """ + ヒストグラムの名前を表現するデータクラス + """ + + title: str + """グラフのタイトル""" + x_axis_label: str + """X軸のラベル名""" + column: str + """pandas.DataFrameにアクセスする列名""" + y_axis_label: str = "タスク数" + """Y軸のラベル名""" class Graph: @@ -70,9 +87,25 @@ def _plot_line_and_circle(fig, x, y, source: ColumnDataSource, username: str, co """ - fig.line(x=x, y=y, source=source, legend_label=username, line_color=color, line_width=1, muted_alpha=0.2, - muted_color=color) - fig.circle(x=x, y=y, source=source, legend_label=username, muted_alpha=0.0, muted_color=color, color=color) + fig.line( + x=x, + y=y, + source=source, + legend_label=username, + line_color=color, + line_width=1, + muted_alpha=0.2, + muted_color=color, + ) + fig.circle( + x=x, + y=y, + source=source, + legend_label=username, + muted_alpha=0.0, + muted_color=color, + color=color, + ) @staticmethod def _set_legend(fig: bokeh.plotting.Figure, hover_tool: HoverTool): @@ -92,6 +125,32 @@ def _set_legend(fig: bokeh.plotting.Figure, hover_tool: HoverTool): legend = fig.legend[0] fig.add_layout(legend, "left") + def _create_histogram(self, df: pd.DataFrame, histogram_name: HistogramName, bins: int = 20) -> hv.Histogram: + """ + ヒストグラムを生成する。 + Args: + df: + histogram_name: + bins: ヒスグラムの棒の数 + + Returns: + ヒストグラムオブジェクト + """ + mean = round(df[histogram_name.column].mean(), 2) + std = round(df[histogram_name.column].std(), 2) + title = f"{histogram_name.title}: μ={mean}, α={std}, N={len(df)}" + + data = df[histogram_name.column].values + + frequencies, edges = np.histogram(data, bins) + hist = hv.Histogram( + (edges, frequencies), + kdims=histogram_name.x_axis_label, + vdims=histogram_name.y_axis_label, + label=title, + ).options(width=500) + return hist + def write_プロジェクト全体のヒストグラム(self, df: pd.DataFrame): """ 以下の情報をヒストグラムで出力する。 @@ -101,49 +160,102 @@ def write_プロジェクト全体のヒストグラム(self, df: pd.DataFrame): df: タスク一覧のDataFrame. Noneならば新たに生成する """ - if len(df) == 0: logger.info("タスク一覧が0件のため出力しない") return - renderer = hv.renderer('bokeh') - - columns = [ - ("annotation_count", "アノテーション数"), - ("sum_worktime_hour", "総作業時間[hour]"), - ("annotation_worktime_hour", "教師付時間[hour]"), - ("inspection_worktime_hour", "検査時間[hour]"), - ("acceptance_worktime_hour", "受入時間[hour]"), - ("inspection_count", "検査コメント数"), - ("input_data_count_of_inspection", "指摘を受けた画像枚数"), - ("input_data_count", "画像枚数"), + renderer = hv.renderer("bokeh") + + histogram_name_list = [ + HistogramName(column="annotation_count", x_axis_label="アノテーション数", title="アノテーション数"), + HistogramName(column="input_data_count", x_axis_label="画像枚数", title="画像枚数"), + HistogramName(column="sum_worktime_hour", x_axis_label="総作業時間[hour]", title="総作業時間"), + HistogramName( + column="annotation_worktime_hour", + x_axis_label="教師付時間[hour]", + title="教師付時間", + ), + HistogramName( + column="inspection_worktime_hour", + x_axis_label="検査時間[hour]", + title="検査時間", + ), + HistogramName( + column="acceptance_worktime_hour", + x_axis_label="受入時間[hour]", + title="受入時間", + ), + HistogramName( + column="first_annotation_worktime_hour", + x_axis_label="1回目の教師付時間[hour]", + title="1回目の教師付時間", + ), + HistogramName( + column="first_inspection_worktime_hour", + x_axis_label="1回目の検査時間[hour]", + title="1回目の検査時間", + ), + HistogramName( + column="first_acceptance_worktime_hour", + x_axis_label="1回目の受入時間[hour]", + title="1回目の受入時間", + ), + HistogramName(column="inspection_count", x_axis_label="検査コメント数", title="検査コメント数"), + HistogramName( + column="input_data_count_of_inspection", + x_axis_label="指摘を受けた画像枚数", + title="指摘を受けた画像枚数", + ), # 経過時間 - ("diff_days_to_first_inspection_started", "最初の検査を着手するまでの日数"), - ("diff_days_to_first_acceptance_started", "最初の受入を着手するまでの日数"), - ("diff_days_to_task_completed", "受入完了状態になるまでの日数"), + HistogramName( + column="diff_days_to_first_inspection_started", + x_axis_label="最初の検査を着手するまでの日数", + title="最初の検査を着手するまでの日数", + ), + HistogramName( + column="diff_days_to_first_acceptance_started", + x_axis_label="最初の受入を着手するまでの日数", + title="最初の受入を着手するまでの日数", + ), + HistogramName( + column="diff_days_to_task_completed", + x_axis_label="受入完了状態になるまでの日数", + title="受入完了状態になるまでの日数", + ), # 差し戻し回数 - ("number_of_rejections_by_inspection", "検査フェーズに対する差し戻し回数"), - ("number_of_rejections_by_acceptance", "受入フェーズに対する差し戻し回数"), + HistogramName( + column="number_of_rejections_by_inspection", + x_axis_label="検査フェーズでの差し戻し回数", + title="検査フェーズでの差し戻し回数", + ), + HistogramName( + column="number_of_rejections_by_acceptance", + x_axis_label="受入フェーズでの差し戻し回数", + title="受入フェーズでの差し戻し回数", + ), ] - histograms1 = [] - for col, y_axis_name in columns: - filtered_df = df[df[col].notnull()] - - mean = round(filtered_df[col].mean(), 2) - std = round(filtered_df[col].std(), 2) - title = f"{y_axis_name}: μ={mean}, α={std}, N={len(filtered_df)}" - - data = filtered_df[col].values - bins = 20 - - frequencies, edges = np.histogram(data, bins) - hist = hv.Histogram((edges, frequencies), kdims=y_axis_name, vdims="タスク数", label=title).options(width=500) - histograms1.append(hist) + histograms = [] + for histogram_name in histogram_name_list: + filtered_df = df[df[histogram_name.column].notnull()] + hist = self._create_histogram(filtered_df, histogram_name=histogram_name) + histograms.append(hist) + + # 自動受入したタスクを除外して、受入時間をグラフ化する + filtered_df = df[df["acceptance_worktime_hour"].notnull()] + histograms.append( + self._create_histogram( + filtered_df[not filtered_df["acceptance_is_skipped"]], + histogram_name=HistogramName( + column="acceptance_worktime_hour", + x_axis_label="受入時間[hour]", + title="受入時間(自動受入されたタスクを除外)", + ), + )) # 軸範囲が同期しないようにする - layout1 = hv.Layout(histograms1).options(shared_axes=False) - renderer.save(layout1, f"{self.outdir}/html/{self.short_project_id}-ヒストグラム-プロジェクト全体") + layout = hv.Layout(histograms).options(shared_axes=False) + renderer.save(layout, f"{self.outdir}/html/{self.short_project_id}-ヒストグラム-プロジェクト全体") def wirte_ラベルごとのアノテーション数(self, df: pd.DataFrame): """ @@ -153,7 +265,7 @@ def wirte_ラベルごとのアノテーション数(self, df: pd.DataFrame): logger.info("タスク一覧が0件のため出力しない") return - renderer = hv.renderer('bokeh') + renderer = hv.renderer("bokeh") histograms2 = [] label_columns = [e for e in df.columns if e.startswith("label_")] @@ -174,8 +286,12 @@ def wirte_ラベルごとのアノテーション数(self, df: pd.DataFrame): layout2 = hv.Layout(histograms2).options(shared_axes=False) renderer.save(layout2, f"{self.outdir}/html/{self.short_project_id}-ヒストグラム-ラベルごとのアノテーション数") - def create_user_id_list(self, df: pd.DataFrame, user_id_column: str, - arg_user_id_list: Optional[List[str]] = None) -> List[str]: + def create_user_id_list( + self, + df: pd.DataFrame, + user_id_column: str, + arg_user_id_list: Optional[List[str]] = None, + ) -> List[str]: """ グラフに表示するユーザのuser_idを生成する。 @@ -201,8 +317,11 @@ def create_user_id_list(self, df: pd.DataFrame, user_id_column: str, return user_id_list[0:max_user_length] - def write_productivity_line_graph_for_annotator(self, df: pd.DataFrame, - first_annotation_user_id_list: Optional[List[str]] = None): + def write_productivity_line_graph_for_annotator( + self, + df: pd.DataFrame, + first_annotation_user_id_list: Optional[List[str]] = None, + ): """ 生産性を教師付作業者ごとにプロットする。 @@ -226,9 +345,14 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) figs: List[bokeh.plotting.Figure] = [] for fig_info in fig_info_list: figs.append( - figure(plot_width=1200, plot_height=600, title=fig_info["title"], - x_axis_label=fig_info["x_axis_label"], x_axis_type="datetime", - y_axis_label=fig_info["y_axis_label"])) + figure( + plot_width=1200, + plot_height=600, + title=fig_info["title"], + x_axis_label=fig_info["x_axis_label"], + x_axis_type="datetime", + y_axis_label=fig_info["y_axis_label"], + )) for user_index, user_id in enumerate(first_annotation_user_id_list): # type: ignore filtered_df = df[df["first_annotation_user_id"] == user_id] @@ -241,16 +365,24 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) username = filtered_df.iloc[0]["first_annotation_username"] for fig, fig_info in zip(figs, fig_info_list): - self._plot_line_and_circle(fig, x=fig_info["x"], y=fig_info["y"], source=source, username=username, - color=color) + self._plot_line_and_circle( + fig, + x=fig_info["x"], + y=fig_info["y"], + source=source, + username=username, + color=color, + ) hover_tool = self._create_hover_tool(tooltip_item) for fig in figs: self._set_legend(fig, hover_tool) bokeh.plotting.reset_output() - bokeh.plotting.output_file(f"{self.outdir}/html/{self.short_project_id}-{html_title}.html", - title=html_title) + bokeh.plotting.output_file( + f"{self.outdir}/html/{self.short_project_id}-{html_title}.html", + title=html_title, + ) bokeh.plotting.save(bokeh.layouts.column(figs)) tooltip_item = [ @@ -279,50 +411,130 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) lambda e: dateutil.parser.parse(e).date()) fig_info_list_annotation_count = [ - dict(x="date_first_annotation_started_date", y="first_annotation_worktime_minute/annotation_count", - title="アノテーションあたり教師付時間(1回目)の折れ線グラフ", x_axis_label="1回目の教師付開始日", - y_axis_label="アノテーションあたり教師付時間(1回目)[min]"), - dict(x="date_first_annotation_started_date", y="annotation_worktime_minute/annotation_count", - title="アノテーションあたり教師付時間の折れ線グラフ", x_axis_label="1回目の教師付開始日", y_axis_label="アノテーションあたり教師付時間[min]"), - dict(x="date_first_annotation_started_date", y="inspection_worktime_minute/annotation_count", - title="アノテーションあたり検査時間の折れ線グラフ", x_axis_label="1回目の教師付開始日", y_axis_label="アノテーションあたり検査時間[min]"), - dict(x="date_first_annotation_started_date", y="acceptance_worktime_minute/annotation_count", - title="アノテーションあたり受入時間の折れ線グラフ", x_axis_label="1回目の教師付開始日", y_axis_label="アノテーションあたり受入時間[min]"), - dict(x="date_first_annotation_started_date", y="inspection_count/annotation_count", - title="アノテーションあたり検査コメント数の折れ線グラフ", x_axis_label="1回目の教師付開始日", y_axis_label="アノテーションあたり検査コメント数"), + dict( + x="date_first_annotation_started_date", + y="first_annotation_worktime_minute/annotation_count", + title="アノテーションあたり教師付時間(1回目)の折れ線グラフ", + x_axis_label="1回目の教師付開始日", + y_axis_label="アノテーションあたり教師付時間(1回目)[min]", + ), + dict( + x="date_first_annotation_started_date", + y="annotation_worktime_minute/annotation_count", + title="アノテーションあたり教師付時間の折れ線グラフ", + x_axis_label="1回目の教師付開始日", + y_axis_label="アノテーションあたり教師付時間[min]", + ), + dict( + x="date_first_annotation_started_date", + y="inspection_worktime_minute/annotation_count", + title="アノテーションあたり検査時間の折れ線グラフ", + x_axis_label="1回目の教師付開始日", + y_axis_label="アノテーションあたり検査時間[min]", + ), + dict( + x="date_first_annotation_started_date", + y="acceptance_worktime_minute/annotation_count", + title="アノテーションあたり受入時間の折れ線グラフ", + x_axis_label="1回目の教師付開始日", + y_axis_label="アノテーションあたり受入時間[min]", + ), + dict( + x="date_first_annotation_started_date", + y="inspection_count/annotation_count", + title="アノテーションあたり検査コメント数の折れ線グラフ", + x_axis_label="1回目の教師付開始日", + y_axis_label="アノテーションあたり検査コメント数", + ), ] - write_cumulative_graph(fig_info_list_annotation_count, html_title="折れ線-横軸_教師付開始日-縦軸_アノテーションあたりの指標-教師付者用") + write_cumulative_graph( + fig_info_list_annotation_count, + html_title="折れ線-横軸_教師付開始日-縦軸_アノテーションあたりの指標-教師付者用", + ) fig_info_list_input_data_count = [ - dict(x="date_first_annotation_started_date", y="first_annotation_worktime_minute/input_data_count", - title="画像あたり教師付時間(1回目)の折れ線グラフ", x_axis_label="1回目の教師付開始日", y_axis_label="画像あたり教師付時間(1回目)[min]"), - dict(x="date_first_annotation_started_date", y="annotation_worktime_minute/input_data_count", - title="画像あたり教師付時間の折れ線グラフ", x_axis_label="1回目の教師付開始日", y_axis_label="画像あたり教師付時間[min]"), - dict(x="date_first_annotation_started_date", y="inspection_worktime_minute/input_data_count", - title="画像あたり検査時間の折れ線グラフ", x_axis_label="1回目の教師付開始日", y_axis_label="画像あたり検査時間[min]"), - dict(x="date_first_annotation_started_date", y="acceptance_worktime_minute/input_data_count", - title="画像あたり受入時間の折れ線グラフ", x_axis_label="1回目の教師付開始日", y_axis_label="画像あたり受入時間[min]"), - dict(x="date_first_annotation_started_date", y="inspection_count/input_data_count", - title="画像あたり検査コメント数の折れ線グラフ", x_axis_label="1回目の教師付開始日", y_axis_label="画像あたり検査コメント数"), + dict( + x="date_first_annotation_started_date", + y="first_annotation_worktime_minute/input_data_count", + title="画像あたり教師付時間(1回目)の折れ線グラフ", + x_axis_label="1回目の教師付開始日", + y_axis_label="画像あたり教師付時間(1回目)[min]", + ), + dict( + x="date_first_annotation_started_date", + y="annotation_worktime_minute/input_data_count", + title="画像あたり教師付時間の折れ線グラフ", + x_axis_label="1回目の教師付開始日", + y_axis_label="画像あたり教師付時間[min]", + ), + dict( + x="date_first_annotation_started_date", + y="inspection_worktime_minute/input_data_count", + title="画像あたり検査時間の折れ線グラフ", + x_axis_label="1回目の教師付開始日", + y_axis_label="画像あたり検査時間[min]", + ), + dict( + x="date_first_annotation_started_date", + y="acceptance_worktime_minute/input_data_count", + title="画像あたり受入時間の折れ線グラフ", + x_axis_label="1回目の教師付開始日", + y_axis_label="画像あたり受入時間[min]", + ), + dict( + x="date_first_annotation_started_date", + y="inspection_count/input_data_count", + title="画像あたり検査コメント数の折れ線グラフ", + x_axis_label="1回目の教師付開始日", + y_axis_label="画像あたり検査コメント数", + ), ] write_cumulative_graph(fig_info_list_input_data_count, html_title="折れ線-横軸_教師付開始日-縦軸_画像あたりの指標-教師付者用") fig_info_list_value = [ - dict(x="date_first_annotation_started_date", y="annotation_count", title="作業したアノテーション数の折れ線グラフ", - x_axis_label="1回目の教師付開始日", y_axis_label="アノテーション数"), - dict(x="date_first_annotation_started_date", y="input_data_count", title="作業した画像数の折れ線グラフ", - x_axis_label="1回目の教師付開始日", y_axis_label="画像数"), - dict(x="date_first_annotation_started_date", y="task_count", title="作業したタスク数の折れ線グラフ", - x_axis_label="1回目の教師付開始日", y_axis_label="タスク数"), - dict(x="date_first_annotation_started_date", y="first_annotation_worktime_hour", title="教師付時間(1回目)の折れ線グラフ", - x_axis_label="1回目の教師付開始日", y_axis_label="教師付時間(1回目)[hour]"), - dict(x="date_first_annotation_started_date", y="annotation_worktime_hour", title="教師付時間の折れ線グラフ", - x_axis_label="1回目の教師付開始日", y_axis_label="教師付時間[hour]"), + dict( + x="date_first_annotation_started_date", + y="annotation_count", + title="作業したアノテーション数の折れ線グラフ", + x_axis_label="1回目の教師付開始日", + y_axis_label="アノテーション数", + ), + dict( + x="date_first_annotation_started_date", + y="input_data_count", + title="作業した画像数の折れ線グラフ", + x_axis_label="1回目の教師付開始日", + y_axis_label="画像数", + ), + dict( + x="date_first_annotation_started_date", + y="task_count", + title="作業したタスク数の折れ線グラフ", + x_axis_label="1回目の教師付開始日", + y_axis_label="タスク数", + ), + dict( + x="date_first_annotation_started_date", + y="first_annotation_worktime_hour", + title="教師付時間(1回目)の折れ線グラフ", + x_axis_label="1回目の教師付開始日", + y_axis_label="教師付時間(1回目)[hour]", + ), + dict( + x="date_first_annotation_started_date", + y="annotation_worktime_hour", + title="教師付時間の折れ線グラフ", + x_axis_label="1回目の教師付開始日", + y_axis_label="教師付時間[hour]", + ), ] write_cumulative_graph(fig_info_list_value, html_title="折れ線-横軸_教師付開始日-縦軸_指標-教師付者用") - def write_cumulative_line_graph_for_annotator(self, df: pd.DataFrame, - first_annotation_user_id_list: Optional[List[str]] = None): + def write_cumulative_line_graph_for_annotator( + self, + df: pd.DataFrame, + first_annotation_user_id_list: Optional[List[str]] = None, + ): """ 教師付作業者用の累積折れ線グラフを出力する。 @@ -347,8 +559,13 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) figs: List[bokeh.plotting.Figure] = [] for fig_info in fig_info_list: figs.append( - figure(plot_width=1200, plot_height=600, title=fig_info["title"], - x_axis_label=fig_info["x_axis_label"], y_axis_label=fig_info["y_axis_label"])) + figure( + plot_width=1200, + plot_height=600, + title=fig_info["title"], + x_axis_label=fig_info["x_axis_label"], + y_axis_label=fig_info["y_axis_label"], + )) for user_index, user_id in enumerate(first_annotation_user_id_list): # type: ignore filtered_df = df[df["first_annotation_user_id"] == user_id] @@ -376,16 +593,24 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) username = filtered_df.iloc[0]["first_annotation_username"] for fig, fig_info in zip(figs, fig_info_list): - self._plot_line_and_circle(fig, x=fig_info["x"], y=fig_info["y"], source=source, username=username, - color=color) + self._plot_line_and_circle( + fig, + x=fig_info["x"], + y=fig_info["y"], + source=source, + username=username, + color=color, + ) hover_tool = self._create_hover_tool(tooltip_item) for fig in figs: self._set_legend(fig, hover_tool) bokeh.plotting.reset_output() - bokeh.plotting.output_file(f"{self.outdir}/html/{self.short_project_id}-{html_title}.html", - title=html_title) + bokeh.plotting.output_file( + f"{self.outdir}/html/{self.short_project_id}-{html_title}.html", + title=html_title, + ) bokeh.plotting.save(bokeh.layouts.column(figs)) tooltip_item = [ @@ -414,51 +639,129 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) # 横軸が累計のアノテーション数 fig_info_list_annotation_count = [ - dict(x="cumulative_annotation_count", y="cumulative_annotation_worktime_hour", title="アノテーション数と教師付時間の累積グラフ", - x_axis_label="アノテーション数", y_axis_label="教師付時間[hour]"), - dict(x="cumulative_annotation_count", y="cumulative_inspection_worktime_hour", title="アノテーション数と検査時間の累積グラフ", - x_axis_label="アノテーション数", y_axis_label="検査時間[hour]"), - dict(x="cumulative_annotation_count", y="cumulative_acceptance_worktime_hour", title="アノテーション数と受入時間の累積グラフ", - x_axis_label="アノテーション数", y_axis_label="受入時間[hour]"), - dict(x="cumulative_annotation_count", y="cumulative_inspection_count", title="アノテーション数と検査コメント数の累積グラフ", - x_axis_label="アノテーション数", y_axis_label="検査コメント数"), + dict( + x="cumulative_annotation_count", + y="cumulative_annotation_worktime_hour", + title="アノテーション数と教師付時間の累積グラフ", + x_axis_label="アノテーション数", + y_axis_label="教師付時間[hour]", + ), + dict( + x="cumulative_annotation_count", + y="cumulative_inspection_worktime_hour", + title="アノテーション数と検査時間の累積グラフ", + x_axis_label="アノテーション数", + y_axis_label="検査時間[hour]", + ), + dict( + x="cumulative_annotation_count", + y="cumulative_acceptance_worktime_hour", + title="アノテーション数と受入時間の累積グラフ", + x_axis_label="アノテーション数", + y_axis_label="受入時間[hour]", + ), + dict( + x="cumulative_annotation_count", + y="cumulative_inspection_count", + title="アノテーション数と検査コメント数の累積グラフ", + x_axis_label="アノテーション数", + y_axis_label="検査コメント数", + ), ] write_cumulative_graph(fig_info_list_annotation_count, html_title="累計折れ線-横軸_アノテーション数-教師付者用") # 横軸が累計の入力データ数 fig_info_list_input_data_count = [ - dict(x="cumulative_input_data_count", y="cumulative_annotation_worktime_hour", title="入力データ数と教師付時間の累積グラフ", - x_axis_label="入力データ数", y_axis_label="教師付時間[hour]"), - dict(x="cumulative_input_data_count", y="cumulative_inspection_worktime_hour", title="入力データ数と検査時間の累積グラフ", - x_axis_label="入力データ数", y_axis_label="検査時間[hour]"), - dict(x="cumulative_input_data_count", y="cumulative_acceptance_worktime_hour", title="入力データ数と受入時間の累積グラフ", - x_axis_label="入力データ数", y_axis_label="受入時間[hour]"), - dict(x="cumulative_input_data_count", y="cumulative_inspection_count", title="アノテーション数と検査コメント数の累積グラフ", - x_axis_label="入力データ数", y_axis_label="検査コメント数"), + dict( + x="cumulative_input_data_count", + y="cumulative_annotation_worktime_hour", + title="入力データ数と教師付時間の累積グラフ", + x_axis_label="入力データ数", + y_axis_label="教師付時間[hour]", + ), + dict( + x="cumulative_input_data_count", + y="cumulative_inspection_worktime_hour", + title="入力データ数と検査時間の累積グラフ", + x_axis_label="入力データ数", + y_axis_label="検査時間[hour]", + ), + dict( + x="cumulative_input_data_count", + y="cumulative_acceptance_worktime_hour", + title="入力データ数と受入時間の累積グラフ", + x_axis_label="入力データ数", + y_axis_label="受入時間[hour]", + ), + dict( + x="cumulative_input_data_count", + y="cumulative_inspection_count", + title="アノテーション数と検査コメント数の累積グラフ", + x_axis_label="入力データ数", + y_axis_label="検査コメント数", + ), ] write_cumulative_graph(fig_info_list_input_data_count, html_title="累計折れ線-横軸_入力データ数-教師付者用") # 横軸が累計のタスク数 fig_info_list_task_count = [ - dict(x="cumulative_task_count", y="cumulative_annotation_worktime_hour", title="タスク数と教師付時間の累積グラフ", - x_axis_label="タスク数", y_axis_label="教師付時間[hour]"), - dict(x="cumulative_task_count", y="cumulative_inspection_worktime_hour", title="タスク数と検査時間の累積グラフ", - x_axis_label="タスク数", y_axis_label="検査時間[hour]"), - dict(x="cumulative_task_count", y="cumulative_acceptance_worktime_hour", title="タスク数と受入時間の累積グラフ", - x_axis_label="タスク数", y_axis_label="受入時間[hour]"), - dict(x="cumulative_task_count", y="cumulative_inspection_count", title="タスク数と検査コメント数の累積グラフ", - x_axis_label="タスク数", y_axis_label="検査コメント数"), - dict(x="cumulative_task_count", y="cumulative_number_of_rejections", title="タスク数と差押し戻し回数の累積グラフ", - x_axis_label="タスク数", y_axis_label="差し戻し回数"), - dict(x="cumulative_task_count", y="cumulative_number_of_rejections_by_inspection", - title="タスク数と差押し戻し回数(検査フェーズ)の累積グラフ", x_axis_label="タスク数", y_axis_label="差し戻し回数(検査フェーズ)"), - dict(x="cumulative_task_count", y="cumulative_number_of_rejections_by_acceptance", - title="タスク数と差押し戻し回数(受入フェーズ)の累積グラフ", x_axis_label="タスク数", y_axis_label="差し戻し回数(受入フェーズ)"), + dict( + x="cumulative_task_count", + y="cumulative_annotation_worktime_hour", + title="タスク数と教師付時間の累積グラフ", + x_axis_label="タスク数", + y_axis_label="教師付時間[hour]", + ), + dict( + x="cumulative_task_count", + y="cumulative_inspection_worktime_hour", + title="タスク数と検査時間の累積グラフ", + x_axis_label="タスク数", + y_axis_label="検査時間[hour]", + ), + dict( + x="cumulative_task_count", + y="cumulative_acceptance_worktime_hour", + title="タスク数と受入時間の累積グラフ", + x_axis_label="タスク数", + y_axis_label="受入時間[hour]", + ), + dict( + x="cumulative_task_count", + y="cumulative_inspection_count", + title="タスク数と検査コメント数の累積グラフ", + x_axis_label="タスク数", + y_axis_label="検査コメント数", + ), + dict( + x="cumulative_task_count", + y="cumulative_number_of_rejections", + title="タスク数と差押し戻し回数の累積グラフ", + x_axis_label="タスク数", + y_axis_label="差し戻し回数", + ), + dict( + x="cumulative_task_count", + y="cumulative_number_of_rejections_by_inspection", + title="タスク数と差押し戻し回数(検査フェーズ)の累積グラフ", + x_axis_label="タスク数", + y_axis_label="差し戻し回数(検査フェーズ)", + ), + dict( + x="cumulative_task_count", + y="cumulative_number_of_rejections_by_acceptance", + title="タスク数と差押し戻し回数(受入フェーズ)の累積グラフ", + x_axis_label="タスク数", + y_axis_label="差し戻し回数(受入フェーズ)", + ), ] write_cumulative_graph(fig_info_list_task_count, html_title="累計折れ線-横軸_タスク数-教師付者用") - def write_cumulative_line_graph_for_inspector(self, df: pd.DataFrame, - first_inspection_user_id_list: Optional[List[str]] = None): + def write_cumulative_line_graph_for_inspector( + self, + df: pd.DataFrame, + first_inspection_user_id_list: Optional[List[str]] = None, + ): """ 検査作業者用の累積折れ線グラフを出力する。 @@ -483,8 +786,13 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) figs: List[bokeh.plotting.Figure] = [] for fig_info in fig_info_list: figs.append( - figure(plot_width=1200, plot_height=600, title=fig_info["title"], - x_axis_label=fig_info["x_axis_label"], y_axis_label=fig_info["y_axis_label"])) + figure( + plot_width=1200, + plot_height=600, + title=fig_info["title"], + x_axis_label=fig_info["x_axis_label"], + y_axis_label=fig_info["y_axis_label"], + )) for user_index, user_id in enumerate(first_inspection_user_id_list): # type: ignore filtered_df = df[df["first_inspection_user_id"] == user_id] @@ -509,16 +817,24 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) username = filtered_df.iloc[0]["first_inspection_username"] for fig, fig_info in zip(figs, fig_info_list): - self._plot_line_and_circle(fig, x=fig_info["x"], y=fig_info["y"], source=source, username=username, - color=color) + self._plot_line_and_circle( + fig, + x=fig_info["x"], + y=fig_info["y"], + source=source, + username=username, + color=color, + ) hover_tool = self._create_hover_tool(tooltip_item) for fig in figs: self._set_legend(fig, hover_tool) bokeh.plotting.reset_output() - bokeh.plotting.output_file(f"{self.outdir}/html/{self.short_project_id}-{html_title}.html", - title=html_title) + bokeh.plotting.output_file( + f"{self.outdir}/html/{self.short_project_id}-{html_title}.html", + title=html_title, + ) bokeh.plotting.save(bokeh.layouts.column(figs)) tooltip_item = [ @@ -553,28 +869,61 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) # 横軸が累計のアノテーション数 fig_info_list_annotation_count = [ - dict(x="cumulative_annotation_count", y="cumulative_first_inspection_worktime_hour", - title="アノテーション数と1回目検査時間の累積グラフ", x_axis_label="アノテーション数", y_axis_label="1回目の検査時間[hour]"), - dict(x="cumulative_annotation_count", y="cumulative_inspection_worktime_hour", title="アノテーション数と検査時間の累積グラフ", - x_axis_label="アノテーション数", y_axis_label="検査時間[hour]"), - dict(x="cumulative_inspection_count", y="cumulative_first_inspection_worktime_hour", - title="検査コメント数と1回目検査時間の累積グラフ", x_axis_label="検査コメント数", y_axis_label="1回目の検査時間[hour]"), - dict(x="cumulative_inspection_count", y="cumulative_inspection_worktime_hour", title="検査コメント数と検査時間の累積グラフ", - x_axis_label="検査コメント数", y_axis_label="検査時間[hour]"), + dict( + x="cumulative_annotation_count", + y="cumulative_first_inspection_worktime_hour", + title="アノテーション数と1回目検査時間の累積グラフ", + x_axis_label="アノテーション数", + y_axis_label="1回目の検査時間[hour]", + ), + dict( + x="cumulative_annotation_count", + y="cumulative_inspection_worktime_hour", + title="アノテーション数と検査時間の累積グラフ", + x_axis_label="アノテーション数", + y_axis_label="検査時間[hour]", + ), + dict( + x="cumulative_inspection_count", + y="cumulative_first_inspection_worktime_hour", + title="検査コメント数と1回目検査時間の累積グラフ", + x_axis_label="検査コメント数", + y_axis_label="1回目の検査時間[hour]", + ), + dict( + x="cumulative_inspection_count", + y="cumulative_inspection_worktime_hour", + title="検査コメント数と検査時間の累積グラフ", + x_axis_label="検査コメント数", + y_axis_label="検査時間[hour]", + ), ] write_cumulative_graph(fig_info_list_annotation_count, html_title="累計折れ線-横軸_アノテーション数-検査者用") # 横軸が累計の入力データ数 fig_info_list_input_data_count = [ - dict(x="cumulative_input_data_count", y="cumulative_first_inspection_worktime_hour", - title="アノテーション数と1回目検査時間の累積グラフ", x_axis_label="入力データ数", y_axis_label="1回目の検査時間[hour]"), - dict(x="cumulative_input_data_count", y="cumulative_inspection_worktime_hour", title="入力データ数と検査時間の累積グラフ", - x_axis_label="入力データ数", y_axis_label="検査時間[hour]"), + dict( + x="cumulative_input_data_count", + y="cumulative_first_inspection_worktime_hour", + title="アノテーション数と1回目検査時間の累積グラフ", + x_axis_label="入力データ数", + y_axis_label="1回目の検査時間[hour]", + ), + dict( + x="cumulative_input_data_count", + y="cumulative_inspection_worktime_hour", + title="入力データ数と検査時間の累積グラフ", + x_axis_label="入力データ数", + y_axis_label="検査時間[hour]", + ), ] write_cumulative_graph(fig_info_list_input_data_count, html_title="累計折れ線-横軸_入力データ数-検査者用") - def write_cumulative_line_graph_for_acceptor(self, df: pd.DataFrame, - first_acceptance_user_id_list: Optional[List[str]] = None): + def write_cumulative_line_graph_for_acceptor( + self, + df: pd.DataFrame, + first_acceptance_user_id_list: Optional[List[str]] = None, + ): """ 受入者用の累積折れ線グラフを出力する。 @@ -599,8 +948,13 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) figs: List[bokeh.plotting.Figure] = [] for fig_info in fig_info_list: figs.append( - figure(plot_width=1200, plot_height=600, title=fig_info["title"], - x_axis_label=fig_info["x_axis_label"], y_axis_label=fig_info["y_axis_label"])) + figure( + plot_width=1200, + plot_height=600, + title=fig_info["title"], + x_axis_label=fig_info["x_axis_label"], + y_axis_label=fig_info["y_axis_label"], + )) for user_index, user_id in enumerate(first_acceptance_user_id_list): # type: ignore filtered_df = df[df["first_acceptance_user_id"] == user_id] @@ -624,16 +978,24 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) username = filtered_df.iloc[0]["first_acceptance_username"] for fig, fig_info in zip(figs, fig_info_list): - self._plot_line_and_circle(fig, x=fig_info["x"], y=fig_info["y"], source=source, username=username, - color=color) + self._plot_line_and_circle( + fig, + x=fig_info["x"], + y=fig_info["y"], + source=source, + username=username, + color=color, + ) hover_tool = self._create_hover_tool(tooltip_item) for fig in figs: self._set_legend(fig, hover_tool) bokeh.plotting.reset_output() - bokeh.plotting.output_file(f"{self.outdir}/html/{self.short_project_id}-{html_title}.html", - title=html_title) + bokeh.plotting.output_file( + f"{self.outdir}/html/{self.short_project_id}-{html_title}.html", + title=html_title, + ) bokeh.plotting.save(bokeh.layouts.column(figs)) tooltip_item = [ @@ -670,22 +1032,52 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) # 横軸が累計のアノテーション数 fig_info_list_annotation_count = [ - dict(x="cumulative_annotation_count", y="cumulative_first_acceptance_worktime_hour", - title="アノテーション数と1回目受入時間の累積グラフ", x_axis_label="アノテーション数", y_axis_label="1回目の受入時間[hour]"), - dict(x="cumulative_annotation_count", y="cumulative_acceptance_worktime_hour", title="アノテーション数と受入時間の累積グラフ", - x_axis_label="アノテーション数", y_axis_label="受入時間[hour]"), - dict(x="cumulative_inspection_count", y="cumulative_first_acceptance_worktime_hour", - title="検査コメント数と1回目受入時間の累積グラフ", x_axis_label="検査コメント数", y_axis_label="1回目の受入時間[hour]"), - dict(x="cumulative_inspection_count", y="cumulative_acceptance_worktime_hour", title="検査コメント数と受入時間の累積グラフ", - x_axis_label="検査コメント数", y_axis_label="受入時間[hour]"), + dict( + x="cumulative_annotation_count", + y="cumulative_first_acceptance_worktime_hour", + title="アノテーション数と1回目受入時間の累積グラフ", + x_axis_label="アノテーション数", + y_axis_label="1回目の受入時間[hour]", + ), + dict( + x="cumulative_annotation_count", + y="cumulative_acceptance_worktime_hour", + title="アノテーション数と受入時間の累積グラフ", + x_axis_label="アノテーション数", + y_axis_label="受入時間[hour]", + ), + dict( + x="cumulative_inspection_count", + y="cumulative_first_acceptance_worktime_hour", + title="検査コメント数と1回目受入時間の累積グラフ", + x_axis_label="検査コメント数", + y_axis_label="1回目の受入時間[hour]", + ), + dict( + x="cumulative_inspection_count", + y="cumulative_acceptance_worktime_hour", + title="検査コメント数と受入時間の累積グラフ", + x_axis_label="検査コメント数", + y_axis_label="受入時間[hour]", + ), ] write_cumulative_graph(fig_info_list_annotation_count, html_title="累計折れ線-横軸_アノテーション数-受入者用") # 横軸が累計の入力データ数 fig_info_list_input_data_count = [ - dict(x="cumulative_input_data_count", y="cumulative_first_acceptance_worktime_hour", - title="アノテーション数と1回目受入時間の累積グラフ", x_axis_label="入力データ数", y_axis_label="1回目の受入時間[hour]"), - dict(x="cumulative_input_data_count", y="cumulative_acceptance_worktime_hour", title="入力データ数と受入時間の累積グラフ", - x_axis_label="入力データ数", y_axis_label="受入時間[hour]"), + dict( + x="cumulative_input_data_count", + y="cumulative_first_acceptance_worktime_hour", + title="アノテーション数と1回目受入時間の累積グラフ", + x_axis_label="入力データ数", + y_axis_label="1回目の受入時間[hour]", + ), + dict( + x="cumulative_input_data_count", + y="cumulative_acceptance_worktime_hour", + title="入力データ数と受入時間の累積グラフ", + x_axis_label="入力データ数", + y_axis_label="受入時間[hour]", + ), ] write_cumulative_graph(fig_info_list_input_data_count, html_title="累計折れ線-横軸_入力データ数-受入者用") diff --git a/annofabcli/statistics/subcommand_statistics.py b/annofabcli/statistics/subcommand_statistics.py index da0d432a..457f7c89 100644 --- a/annofabcli/statistics/subcommand_statistics.py +++ b/annofabcli/statistics/subcommand_statistics.py @@ -7,7 +7,7 @@ def parse_args(parser: argparse.ArgumentParser): - subparsers = parser.add_subparsers(dest='subcommand_name') + subparsers = parser.add_subparsers(dest="subcommand_name") # サブコマンドの定義 annofabcli.statistics.visualize_statistics.add_parser(subparsers) diff --git a/annofabcli/statistics/table.py b/annofabcli/statistics/table.py index 1d9a3ac5..75456d8c 100644 --- a/annofabcli/statistics/table.py +++ b/annofabcli/statistics/table.py @@ -53,8 +53,12 @@ class Table: _worktime_statistics: Optional[List[WorktimeStatistics]] = None _account_statistics: Optional[List[ProjectAccountStatistics]] = None - def __init__(self, database: Database, task_query_param: Dict[str, Any], - ignored_task_id_list: Optional[List[str]] = None): + def __init__( + self, + database: Database, + task_query_param: Dict[str, Any], + ignored_task_id_list: Optional[List[str]] = None, + ): self.annofab_service = database.annofab_service self.annofab_facade = AnnofabApiFacade(database.annofab_service) self.database = database @@ -63,7 +67,7 @@ def __init__(self, database: Database, task_query_param: Dict[str, Any], self.project_id = self.database.project_id self._update_annotaion_specs() - self.project_title = self.annofab_service.api.get_project(self.project_id)[0]['title'] + self.project_title = self.annofab_service.api.get_project(self.project_id)[0]["title"] def _get_worktime_statistics(self) -> List[WorktimeStatistics]: """ @@ -306,10 +310,10 @@ def get_rejections_by_phase(task_histories: List[TaskHistory], phase: TaskPhase) rejections_by_phase = 0 for i, history in enumerate(task_histories): - if history['phase'] != phase.value or history['ended_datetime'] is None: + if history["phase"] != phase.value or history["ended_datetime"] is None: continue - if i + 1 < len(task_histories) and task_histories[i + 1]['phase'] == TaskPhase.ANNOTATION.value: + if (i + 1 < len(task_histories) and task_histories[i + 1]["phase"] == TaskPhase.ANNOTATION.value): rejections_by_phase += 1 return rejections_by_phase @@ -321,7 +325,7 @@ def _set_task_history(self, task: Task, task_history: Optional[TaskHistory], col f"{column_prefix}_user_id": None, f"{column_prefix}_username": None, f"{column_prefix}_started_datetime": None, - f"{column_prefix}_worktime_hour": 0 + f"{column_prefix}_worktime_hour": 0, }) else: account_id = task_history["account_id"] @@ -331,7 +335,7 @@ def _set_task_history(self, task: Task, task_history: Optional[TaskHistory], col f"{column_prefix}_username": self._get_username(account_id), f"{column_prefix}_started_datetime": task_history["started_datetime"], f"{column_prefix}_worktime_hour": isoduration_to_hour( - task_history["accumulated_labor_time_milliseconds"]) + task_history["accumulated_labor_time_milliseconds"]), }) return task @@ -347,20 +351,28 @@ def diff_days(ended_key: str, started_key: str) -> Optional[float]: else: return None + def acceptance_is_skipped(arg_task_histories: List[TaskHistory]) -> bool: + skipped_histories = [ + e for e in arg_task_histories + if (e["phase"] == TaskPhase.ACCEPTANCE.value and e["account_id"] is None + and annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) == 0) + ] + return len(skipped_histories) > 0 + annotation_histories = [e for e in task_histories if e["phase"] == TaskPhase.ANNOTATION.value] inspection_histories = [e for e in task_histories if e["phase"] == TaskPhase.INSPECTION.value] acceptance_histories = [e for e in task_histories if e["phase"] == TaskPhase.ACCEPTANCE.value] # 最初の教師付情報を設定する - first_annotation_history = annotation_histories[0] if len(annotation_histories) > 0 else None + first_annotation_history = (annotation_histories[0] if len(annotation_histories) > 0 else None) self._set_task_history(task, first_annotation_history, column_prefix="first_annotation") # 最初の検査情報を設定する - first_inspection_history = inspection_histories[0] if len(inspection_histories) > 0 else None + first_inspection_history = (inspection_histories[0] if len(inspection_histories) > 0 else None) self._set_task_history(task, first_inspection_history, column_prefix="first_inspection") # 最初の受入情報を設定する - first_acceptance_history = acceptance_histories[0] if len(acceptance_histories) > 0 else None + first_acceptance_history = (acceptance_histories[0] if len(acceptance_histories) > 0 else None) self._set_task_history(task, first_acceptance_history, column_prefix="first_acceptance") task["annotation_worktime_hour"] = sum([ @@ -376,11 +388,11 @@ def diff_days(ended_key: str, started_key: str) -> Optional[float]: task["sum_worktime_hour"] = sum( [annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) for e in task_histories]) - task['number_of_rejections_by_inspection'] = self.get_rejections_by_phase(task_histories, TaskPhase.INSPECTION) - task['number_of_rejections_by_acceptance'] = self.get_rejections_by_phase(task_histories, TaskPhase.ACCEPTANCE) + task["number_of_rejections_by_inspection"] = self.get_rejections_by_phase(task_histories, TaskPhase.INSPECTION) + task["number_of_rejections_by_acceptance"] = self.get_rejections_by_phase(task_histories, TaskPhase.ACCEPTANCE) # 受入完了日時を設定 - if task["phase"] == TaskPhase.ACCEPTANCE.value and task["status"] == TaskStatus.COMPLETE.value: + if (task["phase"] == TaskPhase.ACCEPTANCE.value and task["status"] == TaskStatus.COMPLETE.value): assert len(acceptance_histories) > 0, f"len(acceptance_histories) is 0" task["task_completed_datetime"] = acceptance_histories[-1]["ended_datetime"] else: @@ -393,6 +405,9 @@ def diff_days(ended_key: str, started_key: str) -> Optional[float]: task["diff_days_to_task_completed"] = diff_days("task_completed_datetime", "first_annotation_started_datetime") + # 自動受入されたか否か + task["acceptance_is_skipped"] = acceptance_is_skipped(task_histories) + return task def create_task_history_df(self) -> pd.DataFrame: @@ -532,32 +547,44 @@ def create_dataframe_by_date(task_df: pd.DataFrame) -> pd.DataFrame: # first_annotation_user_id と first_annotation_usernameの両方を指定している理由: # first_annotation_username を取得するため group_obj = new_df.groupby( - ["first_annotation_started_date", "first_annotation_user_id", "first_annotation_username"], as_index=False) + [ + "first_annotation_started_date", + "first_annotation_user_id", + "first_annotation_username", + ], + as_index=False, + ) sum_df = group_obj[[ - "first_annotation_worktime_hour", "annotation_worktime_hour", "inspection_worktime_hour", - "acceptance_worktime_hour", "sum_worktime_hour", "task_count", "input_data_count", "annotation_count", - "inspection_count" + "first_annotation_worktime_hour", + "annotation_worktime_hour", + "inspection_worktime_hour", + "acceptance_worktime_hour", + "sum_worktime_hour", + "task_count", + "input_data_count", + "annotation_count", + "inspection_count", ]].sum() - sum_df["first_annotation_worktime_minute/annotation_count"] = sum_df[ - "first_annotation_worktime_hour"] * 60 / sum_df["annotation_count"] - sum_df["annotation_worktime_minute/annotation_count"] = sum_df["annotation_worktime_hour"] * 60 / sum_df[ - "annotation_count"] - sum_df["inspection_worktime_minute/annotation_count"] = sum_df["inspection_worktime_hour"] * 60 / sum_df[ - "annotation_count"] - sum_df["acceptance_worktime_minute/annotation_count"] = sum_df["acceptance_worktime_hour"] * 60 / sum_df[ - "annotation_count"] - sum_df["inspection_count/annotation_count"] = sum_df["inspection_count"] / sum_df["annotation_count"] - - sum_df["first_annotation_worktime_minute/input_data_count"] = sum_df[ - "first_annotation_worktime_hour"] * 60 / sum_df["input_data_count"] - sum_df["annotation_worktime_minute/input_data_count"] = sum_df["annotation_worktime_hour"] * 60 / sum_df[ - "input_data_count"] - sum_df["inspection_worktime_minute/input_data_count"] = sum_df["inspection_worktime_hour"] * 60 / sum_df[ - "input_data_count"] - sum_df["acceptance_worktime_minute/input_data_count"] = sum_df["acceptance_worktime_hour"] * 60 / sum_df[ - "input_data_count"] - sum_df["inspection_count/input_data_count"] = sum_df["inspection_count"] / sum_df["annotation_count"] + sum_df["first_annotation_worktime_minute/annotation_count"] = (sum_df["first_annotation_worktime_hour"] * 60 / + sum_df["annotation_count"]) + sum_df["annotation_worktime_minute/annotation_count"] = (sum_df["annotation_worktime_hour"] * 60 / + sum_df["annotation_count"]) + sum_df["inspection_worktime_minute/annotation_count"] = (sum_df["inspection_worktime_hour"] * 60 / + sum_df["annotation_count"]) + sum_df["acceptance_worktime_minute/annotation_count"] = (sum_df["acceptance_worktime_hour"] * 60 / + sum_df["annotation_count"]) + sum_df["inspection_count/annotation_count"] = (sum_df["inspection_count"] / sum_df["annotation_count"]) + + sum_df["first_annotation_worktime_minute/input_data_count"] = (sum_df["first_annotation_worktime_hour"] * 60 / + sum_df["input_data_count"]) + sum_df["annotation_worktime_minute/input_data_count"] = (sum_df["annotation_worktime_hour"] * 60 / + sum_df["input_data_count"]) + sum_df["inspection_worktime_minute/input_data_count"] = (sum_df["inspection_worktime_hour"] * 60 / + sum_df["input_data_count"]) + sum_df["acceptance_worktime_minute/input_data_count"] = (sum_df["acceptance_worktime_hour"] * 60 / + sum_df["input_data_count"]) + sum_df["inspection_count/input_data_count"] = (sum_df["inspection_count"] / sum_df["annotation_count"]) return sum_df @@ -577,7 +604,6 @@ def create_member_df(self, task_df: pd.DataFrame) -> pd.DataFrame: "input_data_count_of_first_annotation": 0, "annotation_count_of_first_annotation": 0, "inspection_count_of_first_annotation": 0, - # 関わった作業時間 "annotation_worktime_hour": 0, "inspection_worktime_hour": 0, @@ -629,19 +655,19 @@ def create_account_statistics_df(self): all_histories = [] for account_info in account_statistics: - account_id = account_info['account_id'] - histories = account_info['histories'] + account_id = account_info["account_id"] + histories = account_info["histories"] member_info = self.project_members_dict.get(account_id) for history in histories: - history['worktime_hour'] = annofabcli.utils.isoduration_to_hour(history['worktime']) - history['account_id'] = account_id - history['user_id'] = self._get_user_id(account_id) - history['username'] = self._get_username(account_id) + history["worktime_hour"] = annofabcli.utils.isoduration_to_hour(history["worktime"]) + history["account_id"] = account_id + history["user_id"] = self._get_user_id(account_id) + history["username"] = self._get_username(account_id) if member_info is not None: - history['member_role'] = member_info['member_role'] + history["member_role"] = member_info["member_role"] all_histories.extend(histories) diff --git a/annofabcli/statistics/tsv.py b/annofabcli/statistics/tsv.py index 8059b4a4..fd3dd4cf 100755 --- a/annofabcli/statistics/tsv.py +++ b/annofabcli/statistics/tsv.py @@ -50,8 +50,12 @@ def __init__(self, outdir: str, project_id: str): self.outdir = outdir self.short_project_id = project_id[0:8] - def write_inspection_list(self, df: pd.DataFrame, dropped_columns: Optional[List[str]] = None, - only_error_corrected: bool = True): + def write_inspection_list( + self, + df: pd.DataFrame, + dropped_columns: Optional[List[str]] = None, + only_error_corrected: bool = True, + ): """ 検査コメント一覧をTSVで出力する Args: @@ -114,7 +118,6 @@ def write_task_list(self, df: pd.DataFrame, dropped_columns: List[str] = None): "started_datetime", "updated_datetime", "sampling", - # 最初のアノテーション作業に関すること "first_annotation_user_id", "first_annotation_worktime_hour", @@ -184,12 +187,10 @@ def write_member_list(self, df: pd.DataFrame, dropped_columns: List[str] = None) "username", "member_role", "member_status", - # 関わった作業時間 "annotation_worktime_hour", "inspection_worktime_hour", "acceptance_worktime_hour", - # 初回のアノテーションに関わった個数(タスクの教師付担当者は変更されない前提) "task_count_of_first_annotation", "input_data_count_of_first_annotation", @@ -197,7 +198,7 @@ def write_member_list(self, df: pd.DataFrame, dropped_columns: List[str] = None) "inspection_count_of_first_annotation", ] required_columns = self._create_required_columns(df, prior_columns, dropped_columns) - self._write_csv(f"{self.short_project_id}-メンバ一覧list.csv", df[required_columns]) + self._write_csv(f"{self.short_project_id}-メンバlist.csv", df[required_columns]) def write_ラベルごとのアノテーション数(self, df: pd.DataFrame): """ diff --git a/annofabcli/statistics/visualize_statistics.py b/annofabcli/statistics/visualize_statistics.py index dc642769..5631c488 100644 --- a/annofabcli/statistics/visualize_statistics.py +++ b/annofabcli/statistics/visualize_statistics.py @@ -23,11 +23,11 @@ def write_project_name_file(annofab_service: annofabapi.Resource, project_id: st ファイル名がプロジェクト名のjsonファイルを生成する。 """ project_info = annofab_service.api.get_project(project_id)[0] - project_title = project_info['title'] + project_title = project_info["title"] logger.info(f"project_titile = {project_title}") filename = annofabcli.utils.to_filename(project_title) output_project_dir.mkdir(exist_ok=True, parents=True) - with open(str(output_project_dir / f"{filename}.json"), 'w') as f: + with open(str(output_project_dir / f"{filename}.json"), "w") as f: json.dump(project_info, f, ensure_ascii=False, indent=2) @@ -35,9 +35,18 @@ class VisualizeStatistics(AbstractCommandLineInterface): """ 統計情報を可視化する。 """ - def visualize_statistics(self, project_id: str, work_dir: Path, output_dir: Path, task_query: Dict[str, Any], - ignored_task_id_list: List[str], user_id_list: List[str], update: bool = False, - should_update_annotation_zip: bool = False, should_update_task_json: bool = False): + def visualize_statistics( + self, + project_id: str, + work_dir: Path, + output_dir: Path, + task_query: Dict[str, Any], + ignored_task_id_list: List[str], + user_id_list: List[str], + update: bool = False, + should_update_annotation_zip: bool = False, + should_update_task_json: bool = False, + ): """ タスク一覧を出力する @@ -54,9 +63,12 @@ def visualize_statistics(self, project_id: str, work_dir: Path, output_dir: Path database = Database(self.service, project_id, str(checkpoint_dir)) if update: - database.update_db(task_query, ignored_task_id_list, - should_update_annotation_zip=should_update_annotation_zip, - should_update_task_json=should_update_task_json) + database.update_db( + task_query, + ignored_task_id_list, + should_update_annotation_zip=should_update_annotation_zip, + should_update_task_json=should_update_task_json, + ) table_obj = Table(database, task_query, ignored_task_id_list) write_project_name_file(self.service, project_id, output_dir) @@ -79,7 +91,11 @@ def visualize_statistics(self, project_id: str, work_dir: Path, output_dir: Path tsv_obj.write_task_list(task_df, dropped_columns=["histories_by_phase", "input_data_id_list"]) tsv_obj.write_task_history_list(task_history_df) tsv_obj.write_inspection_list(df=inspection_df, dropped_columns=["data"], only_error_corrected=True) - tsv_obj.write_inspection_list(df=inspection_df_all, dropped_columns=["data"], only_error_corrected=False) + tsv_obj.write_inspection_list( + df=inspection_df_all, + dropped_columns=["data"], + only_error_corrected=False, + ) tsv_obj.write_member_list(member_df) tsv_obj.write_ラベルごとのアノテーション数(annotation_df) @@ -100,14 +116,20 @@ def visualize_statistics(self, project_id: str, work_dir: Path, output_dir: Path try: graph_obj.wirte_ラベルごとのアノテーション数(annotation_df) graph_obj.write_プロジェクト全体のヒストグラム(task_df) - graph_obj.write_cumulative_line_graph_for_annotator(df=task_cumulative_df_by_annotator, - first_annotation_user_id_list=user_id_list) + graph_obj.write_cumulative_line_graph_for_annotator( + df=task_cumulative_df_by_annotator, + first_annotation_user_id_list=user_id_list, + ) - graph_obj.write_cumulative_line_graph_for_inspector(df=task_cumulative_df_by_inspector, - first_inspection_user_id_list=user_id_list) + graph_obj.write_cumulative_line_graph_for_inspector( + df=task_cumulative_df_by_inspector, + first_inspection_user_id_list=user_id_list, + ) - graph_obj.write_cumulative_line_graph_for_acceptor(df=task_cumulative_df_by_acceptor, - first_acceptance_user_id_list=user_id_list) + graph_obj.write_cumulative_line_graph_for_acceptor( + df=task_cumulative_df_by_acceptor, + first_acceptance_user_id_list=user_id_list, + ) graph_obj.write_productivity_line_graph_for_annotator(df=by_date_df, first_annotation_user_id_list=user_id_list) @@ -122,11 +144,17 @@ def main(self): ignored_task_id_list = annofabcli.common.cli.get_list_from_args(args.ignored_task_id) user_id_list = annofabcli.common.cli.get_list_from_args(args.user_id) - self.visualize_statistics(args.project_id, output_dir=Path(args.output_dir), work_dir=Path(args.work_dir), - task_query=task_query, ignored_task_id_list=ignored_task_id_list, - user_id_list=user_id_list, update=not args.not_update, - should_update_annotation_zip=args.update_annotation, - should_update_task_json=args.update_task_json) + self.visualize_statistics( + args.project_id, + output_dir=Path(args.output_dir), + work_dir=Path(args.work_dir), + task_query=task_query, + ignored_task_id_list=ignored_task_id_list, + user_id_list=user_id_list, + update=not args.not_update, + should_update_annotation_zip=args.update_annotation, + should_update_task_json=args.update_task_json, + ) def main(args): @@ -139,34 +167,60 @@ def parse_args(parser: argparse.ArgumentParser): argument_parser = ArgumentParser(parser) argument_parser.add_project_id() - parser.add_argument('-o', '--output_dir', type=str, required=True, help='出力ディレクトリのパス') + parser.add_argument("-o", "--output_dir", type=str, required=True, help="出力ディレクトリのパス") parser.add_argument( - '-u', '--user_id', nargs='+', help=("メンバごとの統計グラフに表示するユーザのuser_idを指定してください。" - "指定しない場合は、辞書順に並べた上位20人が表示されます。" - "file://`を先頭に付けると、一覧が記載されたファイルを指定できます。")) + "-u", + "--user_id", + nargs="+", + help=("メンバごとの統計グラフに表示するユーザのuser_idを指定してください。" + "指定しない場合は、辞書順に並べた上位20人が表示されます。" + "file://`を先頭に付けると、一覧が記載されたファイルを指定できます。"), + ) parser.add_argument( - '-tq', '--task_query', type=str, help='タスクの検索クエリをJSON形式で指定します。指定しない場合はすべてのタスクを取得します。' - '`file://`を先頭に付けると、JSON形式のファイルを指定できます。' - 'クエリのキーは、phase, statusのみです。[getTasks API](https://annofab.com/docs/api/#operation/getTasks) 参照') + "-tq", + "--task_query", + type=str, + help="タスクの検索クエリをJSON形式で指定します。指定しない場合はすべてのタスクを取得します。" + "`file://`を先頭に付けると、JSON形式のファイルを指定できます。" + "クエリのキーは、phase, statusのみです。[getTasks API](https://annofab.com/docs/api/#operation/getTasks) 参照", + ) parser.add_argument( - '--ignored_task_id', nargs='+', help=("可視化対象外のタスクのtask_id。" - "指定しない場合は、すべてのタスクが可視化対象です。" - "file://`を先頭に付けると、一覧が記載されたファイルを指定できます。")) + "--ignored_task_id", + nargs="+", + help=("可視化対象外のタスクのtask_id。" + "指定しない場合は、すべてのタスクが可視化対象です。" + "file://`を先頭に付けると、一覧が記載されたファイルを指定できます。"), + ) - parser.add_argument('--not_update', action="store_true", help='作業ディレクトリ内のファイルを参照して、統計情報を出力します。' - 'AnnoFab Web APIへのアクセスを最小限にします。') + parser.add_argument( + "--not_update", + action="store_true", + help="作業ディレクトリ内のファイルを参照して、統計情報を出力します。" + "AnnoFab Web APIへのアクセスを最小限にします。", + ) parser.add_argument( - '--update_annotation', action="store_true", help='アノテーションzipを更新してから、アノテーションzipをダウンロードします。' - 'ただし、アノテーションzipの最終更新日時がタスクの最終更新日時より新しい場合は、アノテーションzipを更新しません。') + "--update_annotation", + action="store_true", + help="アノテーションzipを更新してから、アノテーションzipをダウンロードします。" + "ただし、アノテーションzipの最終更新日時がタスクの最終更新日時より新しい場合は、アノテーションzipを更新しません。", + ) - parser.add_argument('--update_task_json', action="store_true", help='タスク全件ファイルJSONを更新してから、タスク全件ファイルJSONをダウンロードします。') + parser.add_argument( + "--update_task_json", + action="store_true", + help="タスク全件ファイルJSONを更新してから、タスク全件ファイルJSONをダウンロードします。", + ) - parser.add_argument('--work_dir', type=str, default=".annofab-cli", - help="作業ディレクトリのパス。指定しない場合カレントの'.annofab-cli'ディレクトリに保存する") + parser.add_argument( + "--work_dir", + type=str, + default=".annofab-cli", + help="作業ディレクトリのパス。指定しない場合カレントの'.annofab-cli'ディレクトリに保存する", + ) parser.set_defaults(subcommand_func=main) @@ -174,7 +228,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "visualize" subcommand_help = "統計情報を可視化したファイルを出力します。" - description = ("統計情報を可視化したファイルを出力します。毎日 03:00JST頃に更新されます。") + description = "統計情報を可視化したファイルを出力します。毎日 03:00JST頃に更新されます。" epilog = "チェッカーまたはオーナロールを持つユーザで実行してください。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog) From 72b10a86198d5ce883807c59927653c186e0451e Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Tue, 24 Dec 2019 18:16:31 +0900 Subject: [PATCH 02/19] update setup.py --- setup.py | 60 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/setup.py b/setup.py index eb74a768..7b5f25c0 100644 --- a/setup.py +++ b/setup.py @@ -7,37 +7,42 @@ here = os.path.abspath(os.path.dirname(__file__)) -with open('README.md', 'r', encoding='utf-8') as f: +with open("README.md", "r", encoding="utf-8") as f: readme = f.read() about = {} -with open(os.path.join(here, 'annofabcli', '__version__.py'), 'r', encoding='utf-8') as f: +with open( + os.path.join(here, "annofabcli", "__version__.py"), "r", encoding="utf-8" +) as f: exec(f.read(), about) setup( - name='annofabcli', - version=about['__version__'], - description='Utility Command Line Interface for AnnoFab', + name="annofabcli", + version=about["__version__"], + description="Utility Command Line Interface for AnnoFab", long_description=readme, - long_description_content_type='text/markdown', - author='yuji38kwmt', - author_email='yuji38kwmt@gmail.com', - maintainer='yuji38kwmt', - license='MIT', keywords='annofab api cli', - url='https://github.com/kurusugawa-computer/annofab-cli', - install_requires=['annofabapi>=0.23.1', - 'requests', - 'pillow', - 'pyyaml', - 'dictdiffer', - 'more-itertools', - 'jmespath', - 'pyquery', - 'pandas', - 'isodate', - 'bokeh >=1.4.0', - 'holoviews'], - python_requires='>=3.6', + long_description_content_type="text/markdown", + author="yuji38kwmt", + author_email="yuji38kwmt@gmail.com", + maintainer="yuji38kwmt", + license="MIT", + keywords="annofab api cli", + url="https://github.com/kurusugawa-computer/annofab-cli", + install_requires=[ + "annofabapi>=0.23.2", + "requests", + "pillow", + "pyyaml", + "dictdiffer", + "more-itertools", + "jmespath", + "pyquery", + "pandas", + "isodate", + "bokeh >=1.4.0", + "holoviews", + ], + python_requires=">=3.6", classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", @@ -50,7 +55,6 @@ "Operating System :: OS Independent", ], packages=find_packages(exclude=["tests"]), - package_data={'annofabcli': ['data/logging.yaml']}, - entry_points={ - 'console_scripts': ['annofabcli=annofabcli.__main__:main'], - }) + package_data={"annofabcli": ["data/logging.yaml"]}, + entry_points={"console_scripts": ["annofabcli=annofabcli.__main__:main"],}, +) From 18b7fc753c132c0fe797c57e4e1e94f5cd70b939 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Tue, 24 Dec 2019 20:37:29 +0900 Subject: [PATCH 03/19] =?UTF-8?q?black=E3=82=92=E6=9C=89=E5=8A=B9=E3=81=AB?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- annofabcli/statistics/graph.py | 4 +- annofabcli/statistics/table.py | 299 +++++++++++++++++++++------------ pyproject.toml | 2 + setup.cfg | 3 +- 4 files changed, 197 insertions(+), 111 deletions(-) create mode 100644 pyproject.toml diff --git a/annofabcli/statistics/graph.py b/annofabcli/statistics/graph.py index c045172c..3889d383 100644 --- a/annofabcli/statistics/graph.py +++ b/annofabcli/statistics/graph.py @@ -245,11 +245,11 @@ def write_プロジェクト全体のヒストグラム(self, df: pd.DataFrame): filtered_df = df[df["acceptance_worktime_hour"].notnull()] histograms.append( self._create_histogram( - filtered_df[not filtered_df["acceptance_is_skipped"]], + filtered_df[~ filtered_df["acceptance_is_skipped"]], histogram_name=HistogramName( column="acceptance_worktime_hour", x_axis_label="受入時間[hour]", - title="受入時間(自動受入されたタスクを除外)", + title="受入時間(自動受入タスクを除外)", ), )) diff --git a/annofabcli/statistics/table.py b/annofabcli/statistics/table.py index 75456d8c..c9017b22 100644 --- a/annofabcli/statistics/table.py +++ b/annofabcli/statistics/table.py @@ -1,14 +1,33 @@ import copy import logging from enum import Enum -from typing import Any, Dict, List, Optional, Set, Tuple # pylint: disable=unused-import +from typing import ( + Any, + Dict, + List, + Optional, + Set, + Tuple, +) import dateutil import pandas as pd from annofabapi.dataclass.annotation import SimpleAnnotationDetail -from annofabapi.dataclass.statistics import (ProjectAccountStatistics, ProjectAccountStatisticsHistory, - WorktimeStatistics, WorktimeStatisticsItem) -from annofabapi.models import InputDataId, Inspection, InspectionStatus, Task, TaskHistory, TaskPhase, TaskStatus +from annofabapi.dataclass.statistics import ( + ProjectAccountStatistics, + ProjectAccountStatisticsHistory, + WorktimeStatistics, + WorktimeStatisticsItem, +) +from annofabapi.models import ( + InputDataId, + Inspection, + InspectionStatus, + Task, + TaskHistory, + TaskPhase, + TaskStatus, +) from more_itertools import first_true import annofabcli @@ -54,10 +73,7 @@ class Table: _account_statistics: Optional[List[ProjectAccountStatistics]] = None def __init__( - self, - database: Database, - task_query_param: Dict[str, Any], - ignored_task_id_list: Optional[List[str]] = None, + self, database: Database, task_query_param: Dict[str, Any], ignored_task_id_list: Optional[List[str]] = None, ): self.annofab_service = database.annofab_service self.annofab_facade = AnnofabApiFacade(database.annofab_service) @@ -156,7 +172,7 @@ def _inspection_condition(inspection_arg, exclude_reply: bool, only_error_correc only_error_corrected_flag = True if only_error_corrected: - only_error_corrected_flag = (inspection_arg["status"] == InspectionStatus.ERROR_CORRECTED.value) + only_error_corrected_flag = inspection_arg["status"] == InspectionStatus.ERROR_CORRECTED.value return exclude_reply_flag and only_error_corrected_flag @@ -313,30 +329,48 @@ def get_rejections_by_phase(task_histories: List[TaskHistory], phase: TaskPhase) if history["phase"] != phase.value or history["ended_datetime"] is None: continue - if (i + 1 < len(task_histories) and task_histories[i + 1]["phase"] == TaskPhase.ANNOTATION.value): + if i + 1 < len(task_histories) and task_histories[i + 1]["phase"] == TaskPhase.ANNOTATION.value: rejections_by_phase += 1 return rejections_by_phase - def _set_task_history(self, task: Task, task_history: Optional[TaskHistory], column_prefix: str) -> Task: + def _set_first_phase_from_task_history( + self, task: Task, task_history: Optional[TaskHistory], column_prefix: str + ) -> Task: + """ + 最初のフェーズに関する情報をTask情報に設定する。 + + Args: + task: + task_history: + column_prefix: + + Returns: + + """ if task_history is None: - task.update({ - f"{column_prefix}_account_id": None, - f"{column_prefix}_user_id": None, - f"{column_prefix}_username": None, - f"{column_prefix}_started_datetime": None, - f"{column_prefix}_worktime_hour": 0, - }) + task.update( + { + f"{column_prefix}_account_id": None, + f"{column_prefix}_user_id": None, + f"{column_prefix}_username": None, + f"{column_prefix}_started_datetime": None, + f"{column_prefix}_worktime_hour": 0, + } + ) else: account_id = task_history["account_id"] - task.update({ - f"{column_prefix}_account_id": account_id, - f"{column_prefix}_user_id": self._get_user_id(account_id), - f"{column_prefix}_username": self._get_username(account_id), - f"{column_prefix}_started_datetime": task_history["started_datetime"], - f"{column_prefix}_worktime_hour": isoduration_to_hour( - task_history["accumulated_labor_time_milliseconds"]), - }) + task.update( + { + f"{column_prefix}_account_id": account_id, + f"{column_prefix}_user_id": self._get_user_id(account_id), + f"{column_prefix}_username": self._get_username(account_id), + f"{column_prefix}_started_datetime": task_history["started_datetime"], + f"{column_prefix}_worktime_hour": isoduration_to_hour( + task_history["accumulated_labor_time_milliseconds"] + ), + } + ) return task @@ -344,6 +378,7 @@ def set_task_histories(self, task: Task, task_histories: List[TaskHistory]): """ タスク履歴関係の情報を設定する """ + def diff_days(ended_key: str, started_key: str) -> Optional[float]: if task[ended_key] is not None and task[started_key] is not None: delta = dateutil.parser.parse(task[ended_key]) - dateutil.parser.parse(task[started_key]) @@ -353,9 +388,13 @@ def diff_days(ended_key: str, started_key: str) -> Optional[float]: def acceptance_is_skipped(arg_task_histories: List[TaskHistory]) -> bool: skipped_histories = [ - e for e in arg_task_histories - if (e["phase"] == TaskPhase.ACCEPTANCE.value and e["account_id"] is None - and annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) == 0) + e + for e in arg_task_histories + if ( + e["phase"] == TaskPhase.ACCEPTANCE.value + and e["account_id"] is None + and annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) == 0 + ) ] return len(skipped_histories) > 0 @@ -364,44 +403,72 @@ def acceptance_is_skipped(arg_task_histories: List[TaskHistory]) -> bool: acceptance_histories = [e for e in task_histories if e["phase"] == TaskPhase.ACCEPTANCE.value] # 最初の教師付情報を設定する - first_annotation_history = (annotation_histories[0] if len(annotation_histories) > 0 else None) - self._set_task_history(task, first_annotation_history, column_prefix="first_annotation") + first_annotation_history = annotation_histories[0] if len(annotation_histories) > 0 else None + self._set_first_phase_from_task_history(task, first_annotation_history, column_prefix="first_annotation") # 最初の検査情報を設定する - first_inspection_history = (inspection_histories[0] if len(inspection_histories) > 0 else None) - self._set_task_history(task, first_inspection_history, column_prefix="first_inspection") + first_inspection_history = inspection_histories[0] if len(inspection_histories) > 0 else None + self._set_first_phase_from_task_history(task, first_inspection_history, column_prefix="first_inspection") # 最初の受入情報を設定する - first_acceptance_history = (acceptance_histories[0] if len(acceptance_histories) > 0 else None) - self._set_task_history(task, first_acceptance_history, column_prefix="first_acceptance") - - task["annotation_worktime_hour"] = sum([ - annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) for e in annotation_histories - ]) - task["inspection_worktime_hour"] = sum([ - annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) for e in inspection_histories - ]) - task["acceptance_worktime_hour"] = sum([ - annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) for e in acceptance_histories - ]) + first_acceptance_history = acceptance_histories[0] if len(acceptance_histories) > 0 else None + self._set_first_phase_from_task_history(task, first_acceptance_history, column_prefix="first_acceptance") + + task["annotation_worktime_hour"] = sum( + [ + annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) + for e in annotation_histories + ] + ) + task["inspection_worktime_hour"] = sum( + [ + annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) + for e in inspection_histories + ] + ) + task["acceptance_worktime_hour"] = sum( + [ + annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) + for e in acceptance_histories + ] + ) + + task["first_annotator_worktime_hour"] = ( + sum([e for e in annotation_histories if e["account_id"] == first_annotation_history["account_id"]]) + if first_annotation_history is not None + else 0 + ) + task["first_inspector_worktime_hour"] = ( + sum([e for e in inspection_histories if e["account_id"] == first_inspection_history["account_id"]]) + if first_inspection_history is not None + else 0 + ) + task["first_acceptor_worktime_hour"] = ( + sum([e for e in acceptance_histories if e["account_id"] == first_acceptance_history["account_id"]]) + if first_acceptance_history is not None + else 0 + ) task["sum_worktime_hour"] = sum( - [annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) for e in task_histories]) + [annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) for e in task_histories] + ) task["number_of_rejections_by_inspection"] = self.get_rejections_by_phase(task_histories, TaskPhase.INSPECTION) task["number_of_rejections_by_acceptance"] = self.get_rejections_by_phase(task_histories, TaskPhase.ACCEPTANCE) # 受入完了日時を設定 - if (task["phase"] == TaskPhase.ACCEPTANCE.value and task["status"] == TaskStatus.COMPLETE.value): + if task["phase"] == TaskPhase.ACCEPTANCE.value and task["status"] == TaskStatus.COMPLETE.value: assert len(acceptance_histories) > 0, f"len(acceptance_histories) is 0" task["task_completed_datetime"] = acceptance_histories[-1]["ended_datetime"] else: task["task_completed_datetime"] = None - task["diff_days_to_first_inspection_started"] = diff_days("first_inspection_started_datetime", - "first_annotation_started_datetime") - task["diff_days_to_first_acceptance_started"] = diff_days("first_acceptance_started_datetime", - "first_annotation_started_datetime") + task["diff_days_to_first_inspection_started"] = diff_days( + "first_inspection_started_datetime", "first_annotation_started_datetime" + ) + task["diff_days_to_first_acceptance_started"] = diff_days( + "first_acceptance_started_datetime", "first_annotation_started_datetime" + ) task["diff_days_to_task_completed"] = diff_days("task_completed_datetime", "first_annotation_started_datetime") @@ -426,7 +493,8 @@ def create_task_history_df(self) -> pd.DataFrame: history["user_id"] = self._get_user_id(account_id) history["username"] = self._get_username(account_id) history["worktime_hour"] = annofabcli.utils.isoduration_to_hour( - history["accumulated_labor_time_milliseconds"]) + history["accumulated_labor_time_milliseconds"] + ) all_task_history_list.append(history) df = pd.DataFrame(all_task_history_list) @@ -439,6 +507,7 @@ def create_task_df(self) -> pd.DataFrame: first_annotation_user_id, first_annotation_started_datetime, annotation_worktime_hour, inspection_worktime_hour, acceptance_worktime_hour, sum_worktime_hour """ + def set_annotation_info(arg_task): total_annotation_count = 0 @@ -461,7 +530,8 @@ def set_inspection_info(arg_task): for inspection_list in input_data_dict.values(): # 検査コメントを絞り込む filtered_inspection_list = [ - e for e in inspection_list + e + for e in inspection_list if self._inspection_condition(e, exclude_reply=True, only_error_corrected=True) ] inspection_count += len(filtered_inspection_list) @@ -541,50 +611,56 @@ def create_dataframe_by_date(task_df: pd.DataFrame) -> pd.DataFrame: """ new_df = task_df new_df["first_annotation_started_date"] = new_df["first_annotation_started_datetime"].map( - lambda e: datetime_to_date(e) if e is not None else None) + lambda e: datetime_to_date(e) if e is not None else None + ) new_df["task_count"] = 1 # 集計用 # first_annotation_user_id と first_annotation_usernameの両方を指定している理由: # first_annotation_username を取得するため group_obj = new_df.groupby( + ["first_annotation_started_date", "first_annotation_user_id", "first_annotation_username"], as_index=False, + ) + sum_df = group_obj[ [ - "first_annotation_started_date", - "first_annotation_user_id", - "first_annotation_username", - ], - as_index=False, + "first_annotation_worktime_hour", + "annotation_worktime_hour", + "inspection_worktime_hour", + "acceptance_worktime_hour", + "sum_worktime_hour", + "task_count", + "input_data_count", + "annotation_count", + "inspection_count", + ] + ].sum() + + sum_df["first_annotation_worktime_minute/annotation_count"] = ( + sum_df["first_annotation_worktime_hour"] * 60 / sum_df["annotation_count"] + ) + sum_df["annotation_worktime_minute/annotation_count"] = ( + sum_df["annotation_worktime_hour"] * 60 / sum_df["annotation_count"] ) - sum_df = group_obj[[ - "first_annotation_worktime_hour", - "annotation_worktime_hour", - "inspection_worktime_hour", - "acceptance_worktime_hour", - "sum_worktime_hour", - "task_count", - "input_data_count", - "annotation_count", - "inspection_count", - ]].sum() - - sum_df["first_annotation_worktime_minute/annotation_count"] = (sum_df["first_annotation_worktime_hour"] * 60 / - sum_df["annotation_count"]) - sum_df["annotation_worktime_minute/annotation_count"] = (sum_df["annotation_worktime_hour"] * 60 / - sum_df["annotation_count"]) - sum_df["inspection_worktime_minute/annotation_count"] = (sum_df["inspection_worktime_hour"] * 60 / - sum_df["annotation_count"]) - sum_df["acceptance_worktime_minute/annotation_count"] = (sum_df["acceptance_worktime_hour"] * 60 / - sum_df["annotation_count"]) - sum_df["inspection_count/annotation_count"] = (sum_df["inspection_count"] / sum_df["annotation_count"]) - - sum_df["first_annotation_worktime_minute/input_data_count"] = (sum_df["first_annotation_worktime_hour"] * 60 / - sum_df["input_data_count"]) - sum_df["annotation_worktime_minute/input_data_count"] = (sum_df["annotation_worktime_hour"] * 60 / - sum_df["input_data_count"]) - sum_df["inspection_worktime_minute/input_data_count"] = (sum_df["inspection_worktime_hour"] * 60 / - sum_df["input_data_count"]) - sum_df["acceptance_worktime_minute/input_data_count"] = (sum_df["acceptance_worktime_hour"] * 60 / - sum_df["input_data_count"]) - sum_df["inspection_count/input_data_count"] = (sum_df["inspection_count"] / sum_df["annotation_count"]) + sum_df["inspection_worktime_minute/annotation_count"] = ( + sum_df["inspection_worktime_hour"] * 60 / sum_df["annotation_count"] + ) + sum_df["acceptance_worktime_minute/annotation_count"] = ( + sum_df["acceptance_worktime_hour"] * 60 / sum_df["annotation_count"] + ) + sum_df["inspection_count/annotation_count"] = sum_df["inspection_count"] / sum_df["annotation_count"] + + sum_df["first_annotation_worktime_minute/input_data_count"] = ( + sum_df["first_annotation_worktime_hour"] * 60 / sum_df["input_data_count"] + ) + sum_df["annotation_worktime_minute/input_data_count"] = ( + sum_df["annotation_worktime_hour"] * 60 / sum_df["input_data_count"] + ) + sum_df["inspection_worktime_minute/input_data_count"] = ( + sum_df["inspection_worktime_hour"] * 60 / sum_df["input_data_count"] + ) + sum_df["acceptance_worktime_minute/input_data_count"] = ( + sum_df["acceptance_worktime_hour"] * 60 / sum_df["input_data_count"] + ) + sum_df["inspection_count/input_data_count"] = sum_df["inspection_count"] / sum_df["annotation_count"] return sum_df @@ -598,17 +674,19 @@ def create_member_df(self, task_df: pd.DataFrame) -> pd.DataFrame: member_dict = {} for account_id, member in self.project_members_dict.items(): new_member = copy.deepcopy(member) - new_member.update({ - # 初回のアノテーションに関わった個数(タスクの教師付担当者は変更されない前提) - "task_count_of_first_annotation": 0, - "input_data_count_of_first_annotation": 0, - "annotation_count_of_first_annotation": 0, - "inspection_count_of_first_annotation": 0, - # 関わった作業時間 - "annotation_worktime_hour": 0, - "inspection_worktime_hour": 0, - "acceptance_worktime_hour": 0, - }) + new_member.update( + { + # 初回のアノテーションに関わった個数(タスクの教師付担当者は変更されない前提) + "task_count_of_first_annotation": 0, + "input_data_count_of_first_annotation": 0, + "annotation_count_of_first_annotation": 0, + "inspection_count_of_first_annotation": 0, + # 関わった作業時間 + "annotation_worktime_hour": 0, + "inspection_worktime_hour": 0, + "acceptance_worktime_hour": 0, + } + ) member_dict[account_id] = new_member for _, row_task in task_df.iterrows(): @@ -682,8 +760,9 @@ def create_cumulative_df_by_first_annotator(task_df: pd.DataFrame) -> pd.DataFra task_df: タスク一覧のDataFrame. 列が追加される """ # 教師付の開始時刻でソートして、indexを更新する - df = task_df.sort_values(["first_annotation_account_id", - "first_annotation_started_datetime"]).reset_index(drop=True) + df = task_df.sort_values(["first_annotation_account_id", "first_annotation_started_datetime"]).reset_index( + drop=True + ) # タスクの累計数を取得するために設定する df["task_count"] = 1 # 教師付の作業者でgroupby @@ -716,8 +795,9 @@ def create_cumulative_df_by_first_inspector(task_df: pd.DataFrame) -> pd.DataFra task_df: タスク一覧のDataFrame. 列が追加される """ - df = task_df.sort_values(["first_inspection_account_id", - "first_inspection_started_datetime"]).reset_index(drop=True) + df = task_df.sort_values(["first_inspection_account_id", "first_inspection_started_datetime"]).reset_index( + drop=True + ) # タスクの累計数を取得するために設定する df["task_count"] = 1 # 教師付の作業者でgroupby @@ -750,8 +830,9 @@ def create_cumulative_df_by_first_acceptor(task_df: pd.DataFrame) -> pd.DataFram task_df: タスク一覧のDataFrame. 列が追加される """ - df = task_df.sort_values(["first_acceptance_account_id", - "first_acceptance_started_datetime"]).reset_index(drop=True) + df = task_df.sort_values(["first_acceptance_account_id", "first_acceptance_started_datetime"]).reset_index( + drop=True + ) # タスクの累計数を取得するために設定する df["task_count"] = 1 groupby_obj = df.groupby("first_acceptance_account_id") @@ -803,5 +884,7 @@ def create_worktime_per_image_df(self, aggregation_by: AggregationBy, phase: Tas df = pd.DataFrame(worktime_info_list) # acount_idをusernameに変更する - columns = {col: self._get_username(col) for col in df.columns if col != "date"} # pylint: disable=not-an-iterable # noqa: E501 + columns = { + col: self._get_username(col) for col in df.columns if col != "date" # pylint: disable=not-an-iterable + } return df.rename(columns=columns).fillna(0) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..55ec8d78 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.black] +line-length = 120 diff --git a/setup.cfg b/setup.cfg index 97940676..94eea0fb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,7 +11,7 @@ split_before_named_assigns = False [flake8] max-line-length = 120 # F401: 'xxxx' imported but unused -ignore = F401, E124 +ignore = F401, E124, W503 [mypy] ignore_missing_imports = True @@ -59,3 +59,4 @@ disable = # ===== Custom ===== protected-access, logging-format-interpolation, + C0330, From 7dd6f39c20ca9e9d26ce89a721976cf7ed222cc8 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Tue, 24 Dec 2019 21:00:34 +0900 Subject: [PATCH 04/19] =?UTF-8?q?[statistics]=201=E5=9B=9E=E7=9B=AE?= =?UTF-8?q?=E3=81=AE=E6=95=99=E5=B8=AB=E4=BB=98=E8=80=85=E3=81=AE=E6=99=82?= =?UTF-8?q?=E9=96=93=E3=82=92=E7=AE=97=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Pipfile | 1 + Pipfile.lock | 265 ++++++++++++++++++++------------- annofabcli/statistics/graph.py | 15 ++ annofabcli/statistics/table.py | 4 +- 4 files changed, 181 insertions(+), 104 deletions(-) diff --git a/Pipfile b/Pipfile index e52c4df2..15218db2 100644 --- a/Pipfile +++ b/Pipfile @@ -15,6 +15,7 @@ twine = "*" autoflake = "*" flake8 = "*" docutils = "*" +black = "*" [packages] diff --git a/Pipfile.lock b/Pipfile.lock index dc8df450..7a14f127 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "26235be55c8558ca5cb241942444f3967fe07003c6eacb686ff65966c267206c" + "sha256": "dc71b79c8d883a14fc726a96d6f43a4f76d84bb029c69799a8fbbdd504dc4820" }, "pipfile-spec": 6, "requires": {}, @@ -17,15 +17,22 @@ "develop": { "annofabapi": { "hashes": [ - "sha256:b70f0b34a1e1da01a0c063a284d309c92f0edf1b3c347c982168162977db7dd7", - "sha256:c6e00140716fcc850185442938a6b973ba25240ddb99d0cac95f5a1f66d5a658" + "sha256:884801473ef5bde080a34f0f95e22ebba5ab30da46b4c2c781179eaa1be4044c", + "sha256:f043a44e3d39de620460f79f38718b56e009718cfccde2caf930a6c986e6067d" ], - "version": "==0.23.1" + "version": "==0.23.2" }, "annofabcli": { "editable": true, "path": "." }, + "appdirs": { + "hashes": [ + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + ], + "version": "==1.4.3" + }, "astroid": { "hashes": [ "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", @@ -54,6 +61,14 @@ ], "version": "==1.10.0" }, + "black": { + "hashes": [ + "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", + "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539" + ], + "index": "pypi", + "version": "==19.10b0" + }, "bleach": { "hashes": [ "sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16", @@ -119,41 +134,48 @@ ], "version": "==3.0.4" }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, "coverage": { "hashes": [ - "sha256:018e74df50a58fd2aaa7efeb301d1599d8fd3cc5c92388506e45a2bd0154c003", - "sha256:0e08415f35cc57b6eb93fb31ec48a2a169ab838e12ac22106cf9baee4938e46d", - "sha256:1f9c1d9692339a7ec5cf3ba4475fb648675f438db3413b3d98e2c9ed30ac956c", - "sha256:2643ec874d3aa30a65a36e42042424ce08f80a948e1c942d3d787489ae318a64", - "sha256:28a19392a6c4616a76e059a03118c8cca28af46d9e72032590ce329e6b30ac40", - "sha256:32c6be8ba90aa885a4900a0187de44f51730c0660c45de3b943c40f1547d47c2", - "sha256:4a8b839c3a5579502ffecd4519533d406ad2bec1488724cee4b2c79dd6cbea79", - "sha256:5061a8c4bba83b7613077d6cfa8e81381c80f134dc7f02853c3235b38f76b8a6", - "sha256:54770fe39bb4718b5a07665e1f4c676382d6927299ab736149618f0d37d72e85", - "sha256:5a237fa332721721545e88fbed42e79acbe9c77be8310c167ec5449df44dac7c", - "sha256:607ed848b1373b161629d0c8228d90dd47ecb342f1f800dc41ebd0bce2432e24", - "sha256:65929e8d15999450d09117114ee185948bc77637e868daff5f5ab47219e1e7b6", - "sha256:67a0f5c50503a6226e28fb7ee3c3aa1a104681460d5123eeb80e6afb65ba46a5", - "sha256:6a70362452ea9c09efe1a7faa365f6603f4a0ef54306a6e5a46e6e32913536fc", - "sha256:6cf4502b0087f06906059a718e02b231a9f611ae34794a955baa2e443d5064ad", - "sha256:73f596fcd93d76579b4aefa53b7cf5df90d953a8fd94ebfc9b36d0ba47db4236", - "sha256:77c90b0a221e6355c771f1b9a6ed45c384f9dff00836823a732f5fe4224cb43a", - "sha256:8524e47b78fc7eea96a25717e779c5e6657536515dd57dfd1110aff34dc747b6", - "sha256:85b8db2e8c7e9bdf44e4b5859be2cbeaf73e54cf7cecd6c6705f010110581840", - "sha256:90389fea98570dc1f155ceae40972fcf798954467c69d810e385b170c34205cd", - "sha256:9e48182563c7861c47593a4b931a6d57e4e499027913e5fdc61efc0d368e804a", - "sha256:a5a5f3a9167a5316d675932e455925e136b1e33d15ce48692b94af746736260e", - "sha256:a7f3be4952f25a0cb8c275cdf064c3ba1765e370f337b015e84a00bb6244c86b", - "sha256:b2de4918d6d4aea7fe2b6ef778190f60ff4355248045d7b1fbb35922e0ceb39e", - "sha256:b72f1b6f27cf67b74370cc9df6b7f47546669861aad660587a7c6f2a01728840", - "sha256:b9b8189f6f8c4a2a09142d95a11f91dff2cac30c9c0b5ab61e3e0a785e960b94", - "sha256:bd31b194b42e0de4c29fd2c56ad0c493639bcb792c48b694b18568f65f0c2c6d", - "sha256:be732dcc0e9ca31a15ab5ba116ed7af9b5a046d035a00555ad593f4ddfeb7a00", - "sha256:bf4131b04dc2bf35e091b2c759d9e741876a235427c001c6d20147cf29797691", - "sha256:d3021b26f86118c447afccbd53b443dc25b4848f9ae49ffb4b9588cb5110d360", - "sha256:d500eb1db73cd5cfe28755c790fcb4e3c653b70d30d0ddf79fde3cc603d3789f" - ], - "version": "==5.0b2" + "sha256:0101888bd1592a20ccadae081ba10e8b204d20235d18d05c6f7d5e904a38fc10", + "sha256:04b961862334687549eb91cd5178a6fbe977ad365bddc7c60f2227f2f9880cf4", + "sha256:1ca43dbd739c0fc30b0a3637a003a0d2c7edc1dd618359d58cc1e211742f8bd1", + "sha256:1cbb88b34187bdb841f2599770b7e6ff8e259dc3bb64fc7893acf44998acf5f8", + "sha256:232f0b52a5b978288f0bbc282a6c03fe48cd19a04202df44309919c142b3bb9c", + "sha256:24bcfa86fd9ce86b73a8368383c39d919c497a06eebb888b6f0c12f13e920b1a", + "sha256:25b8f60b5c7da71e64c18888f3067d5b6f1334b9681876b2fb41eea26de881ae", + "sha256:2714160a63da18aed9340c70ed514973971ee7e665e6b336917ff4cca81a25b1", + "sha256:2ca2cd5264e84b2cafc73f0045437f70c6378c0d7dbcddc9ee3fe192c1e29e5d", + "sha256:2cc707fc9aad2592fc686d63ef72dc0031fc98b6fb921d2f5395d9ab84fbc3ef", + "sha256:348630edea485f4228233c2f310a598abf8afa5f8c716c02a9698089687b6085", + "sha256:40fbfd6b044c9db13aeec1daf5887d322c710d811f944011757526ef6e323fd9", + "sha256:46c9c6a1d1190c0b75ec7c0f339088309952b82ae8d67a79ff1319eb4e749b96", + "sha256:591506e088901bdc25620c37aec885e82cc896528f28c57e113751e3471fc314", + "sha256:5ac71bba1e07eab403b082c4428f868c1c9e26a21041436b4905c4c3d4e49b08", + "sha256:5f622f19abda4e934938e24f1d67599249abc201844933a6f01aaa8663094489", + "sha256:65bead1ac8c8930cf92a1ccaedcce19a57298547d5d1db5c9d4d068a0675c38b", + "sha256:7362a7f829feda10c7265b553455de596b83d1623b3d436b6d3c51c688c57bf6", + "sha256:7f2675750c50151f806070ec11258edf4c328340916c53bac0adbc465abd6b1e", + "sha256:960d7f42277391e8b1c0b0ae427a214e1b31a1278de6b73f8807b20c2e913bba", + "sha256:a50b0888d8a021a3342d36a6086501e30de7d840ab68fca44913e97d14487dc1", + "sha256:b7dbc5e8c39ea3ad3db22715f1b5401cd698a621218680c6daf42c2f9d36e205", + "sha256:bb3d29df5d07d5399d58a394d0ef50adf303ab4fbf66dfd25b9ef258effcb692", + "sha256:c0fff2733f7c2950f58a4fd09b5db257b00c6fec57bf3f68c5bae004d804b407", + "sha256:c792d3707a86c01c02607ae74364854220fb3e82735f631cd0a345dea6b4cee5", + "sha256:c90bda74e16bcd03861b09b1d37c0a4158feda5d5a036bb2d6e58de6ff65793e", + "sha256:cfce79ce41cc1a1dc7fc85bb41eeeb32d34a4cf39a645c717c0550287e30ff06", + "sha256:eeafb646f374988c22c8e6da5ab9fb81367ecfe81c70c292623373d2a021b1a1", + "sha256:f425f50a6dd807cb9043d15a4fcfba3b5874a54d9587ccbb748899f70dc18c47", + "sha256:fcd4459fe35a400b8f416bc57906862693c9f88b66dc925e7f2a933e77f6b18b", + "sha256:ff3936dd5feaefb4f91c8c1f50a06c588b5dc69fba4f7d9c79a6617ad80bb7df" + ], + "version": "==5.0.1" }, "cryptography": { "hashes": [ @@ -190,25 +212,25 @@ }, "dataclasses-json": { "hashes": [ - "sha256:616876ac06ca3a8f2ae977a4e044850e81dbd3f86a404430b783e12f017b75b5", - "sha256:ebdf7407681763d6125fd00d15ed037cc3aa6f9129fe7634e8f891410e89559f" + "sha256:8e7b68ee574bd67e305cbe06acac74137e978529fd1ea85184baedbf47ff0d49", + "sha256:a11bda62235094cc8385357a125be28233a20d4296274c44bb766b2f9f1e5d98" ], - "version": "==0.3.6" + "version": "==0.3.7" }, "dictdiffer": { "hashes": [ - "sha256:97cf4ef98ebc1acf737074aed41e379cf48ab5ff528c92109dfb8e2e619e6809", - "sha256:b3ad476fc9cca60302b52c50e1839342d2092aeaba586d69cbf9249f87f52463" + "sha256:1adec0d67cdf6166bda96ae2934ddb5e54433998ceab63c984574d187cc563d2", + "sha256:d79d9a39e459fe33497c858470ca0d2e93cb96621751de06d631856adfd9c390" ], - "version": "==0.8.0" + "version": "==0.8.1" }, "docutils": { "hashes": [ - "sha256:7a6228589435302e421f5c473ce0180878b90f70227f7174cacde5efbd34275f", - "sha256:f1bad547016f945f7b35b28d8bead307821822ca3f8d4f87a1bd2ad1a8faab51" + "sha256:6dd0078bcfecbef00123531c6b0ca38460b291334ef8dd05c00e060e90ead6ce", + "sha256:d33b326920f238f2149951e0f2ca0513ea660217b07fe63cb8c5e74e78b3114f" ], "index": "pypi", - "version": "==0.16b0.dev0" + "version": "==0.16rc1" }, "entrypoints": { "hashes": [ @@ -410,23 +432,23 @@ }, "mypy": { "hashes": [ - "sha256:02d9bdd3398b636723ecb6c5cfe9773025a9ab7f34612c1cde5c7f2292e2d768", - "sha256:088f758a50af31cf8b42688118077292370c90c89232c783ba7979f39ea16646", - "sha256:28e9fbc96d13397a7ddb7fad7b14f373f91b5cff538e0772e77c270468df083c", - "sha256:30e123b24931f02c5d99307406658ac8f9cd6746f0d45a3dcac2fe5fbdd60939", - "sha256:3294821b5840d51a3cd7a2bb63b40fc3f901f6a3cfb3c6046570749c4c7ef279", - "sha256:41696a7d912ce16fdc7c141d87e8db5144d4be664a0c699a2b417d393994b0c2", - "sha256:4f42675fa278f3913340bb8c3371d191319704437758d7c4a8440346c293ecb2", - "sha256:54d205ccce6ed930a8a2ccf48404896d456e8b87812e491cb907a355b1a9c640", - "sha256:6992133c95a2847d309b4b0c899d7054adc60481df6f6b52bb7dee3d5fd157f7", - "sha256:6ecbd0e8e371333027abca0922b0c2c632a5b4739a0c61ffbd0733391e39144c", - "sha256:83fa87f556e60782c0fc3df1b37b7b4a840314ba1ac27f3e1a1e10cb37c89c17", - "sha256:c87ac7233c629f305602f563db07f5221950fe34fe30af072ac838fa85395f78", - "sha256:de9ec8dba773b78c49e7bec9a35c9b6fc5235682ad1fc2105752ae7c22f4b931", - "sha256:f385a0accf353ca1bca4bbf473b9d83ed18d923fdb809d3a70a385da23e25b6a" + "sha256:0a9a45157e532da06fe56adcfef8a74629566b607fa2c1ac0122d1ff995c748a", + "sha256:2c35cae79ceb20d47facfad51f952df16c2ae9f45db6cb38405a3da1cf8fc0a7", + "sha256:4b9365ade157794cef9685791032521233729cb00ce76b0ddc78749abea463d2", + "sha256:53ea810ae3f83f9c9b452582261ea859828a9ed666f2e1ca840300b69322c474", + "sha256:634aef60b4ff0f650d3e59d4374626ca6153fcaff96ec075b215b568e6ee3cb0", + "sha256:7e396ce53cacd5596ff6d191b47ab0ea18f8e0ec04e15d69728d530e86d4c217", + "sha256:7eadc91af8270455e0d73565b8964da1642fe226665dd5c9560067cd64d56749", + "sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6", + "sha256:85baab8d74ec601e86134afe2bcccd87820f79d2f8d5798c889507d1088287bf", + "sha256:87c556fb85d709dacd4b4cb6167eecc5bbb4f0a9864b69136a0d4640fdc76a36", + "sha256:a6bd44efee4dc8c3324c13785a9dc3519b3ee3a92cada42d2b57762b7053b49b", + "sha256:c6d27bd20c3ba60d5b02f20bd28e20091d6286a699174dfad515636cb09b5a72", + "sha256:e2bb577d10d09a2d8822a042a23b8d62bc3b269667c9eb8e60a6edfa000211b1", + "sha256:f97a605d7c8bc2c6d1172c2f0d5a65b24142e11a58de689046e62c2d632ca8c1" ], "index": "pypi", - "version": "==0.750" + "version": "==0.761" }, "mypy-extensions": { "hashes": [ @@ -437,29 +459,29 @@ }, "numpy": { "hashes": [ - "sha256:2bc9c62dfc893626cdd50dce30f597f387c4aca11cd2cdffe3e7e06a4fe19ee6", - "sha256:3701ddb007a0549c12b26e90c82d520b9c4acf801705b9334654ade2a9550b75", - "sha256:4c51a496ec1ce170f2b5eb458c0441affc5b1fd1d7272cf322b443c90c69983a", - "sha256:6beebdc222e214bbbafab8f089a65821cff6cad25b349eabb653490dc25342fd", - "sha256:70319f4d4e0a0c94e04922aaaf8c9aca72d38b61cc69e338fb872e9ff27d94a2", - "sha256:73abd855401b9da6efb67490f4dd82226cf95f47d66b8dc9ebe4df523baaaeb2", - "sha256:76d92b0b86227c6dbc3f9030b8b865f41ae04f73daa161e4e3c5566e03d6ed41", - "sha256:787781333c1d69c7c23ccd85165cca732a5f3fd9d997b8ee40829b7c0c38db86", - "sha256:78ca6befab03c682bcb013241801e3b750e9de9b60664e9839e5b2098b5580ee", - "sha256:7b0b915190cf60e691c17147f5d955e273d4c482b795a7bb168ad4a2fe2fb180", - "sha256:9b369822f7681bc36b6ba624bc8ea0a1a456a9f72b324070d89ee2856cba62be", - "sha256:a8081de993fd47b9cb7376935bb1781118fd2c473a0e834601e28229275f78d6", - "sha256:c207646355d1d04c054d781ddcd100bbd5afc69f75f8a7623317b9db41a2015f", - "sha256:c9474a8fe03ca958e6fafefee13d6b4a45ea4ed7e35261abf61899c0f599a118", - "sha256:c9e73aa60166609c80f4285af8ba8b9b79e5a935df6927a174637c7c08fa8e67", - "sha256:cbd2e1c1fb61b17eca745d7f9f6d684fd7f7817bef6454890eb6fa3e1cd4905a", - "sha256:d7b16541a6e970d402587ff2f1cbd85753c8d982a0d5894991505fea9b81a52b", - "sha256:e89027b24027dade03929bc17adee23feec1f6f707f4ea366c5d5a342d4d81a1", - "sha256:e9907b5ea505dcd2cfaeb6ab08f241047ba651611f3974c9d624a7c5066ab3a6", - "sha256:f676739da486d1c7de2d9450dafca6dee04f4d3e881b8761b795865ef1872eaf", - "sha256:fa8851d10af0739adcae54acf5706953e2e45752f4a550006f6f3aff92335566" - ], - "version": "==1.18.0rc1" + "sha256:03bbde29ac8fba860bb2c53a1525b3604a9b60417855ac3119d89868ec6041c3", + "sha256:1baefd1fb4695e7f2e305467dbd876d765e6edd30c522894df76f8301efaee36", + "sha256:1c35fb1131362e6090d30286cfda52ddd42e69d3e2bf1fea190a0fad83ea3a18", + "sha256:3c68c827689ca0ca713dba598335073ce0966850ec0b30715527dce4ecd84055", + "sha256:443ab93fc35b31f01db8704681eb2fd82f3a1b2fa08eed2dd0e71f1f57423d4a", + "sha256:56710a756c5009af9f35b91a22790701420406d9ac24cf6b652b0e22cfbbb7ff", + "sha256:62506e9e4d2a39c87984f081a2651d4282a1d706b1a82fe9d50a559bb58e705a", + "sha256:6f8113c8dbfc192b58996ee77333696469ea121d1c44ea429d8fd266e4c6be51", + "sha256:712f0c32555132f4b641b918bdb1fd3c692909ae916a233ce7f50eac2de87e37", + "sha256:854f6ed4fa91fa6da5d764558804ba5b0f43a51e5fe9fc4fdc93270b052f188a", + "sha256:88c5ccbc4cadf39f32193a5ef22e3f84674418a9fd877c63322917ae8f295a56", + "sha256:905cd6fa6ac14654a6a32b21fad34670e97881d832e24a3ca32e19b455edb4a8", + "sha256:9d6de2ad782aae68f7ed0e0e616477fbf693d6d7cc5f0f1505833ff12f84a673", + "sha256:a30f5c3e1b1b5d16ec1f03f4df28e08b8a7529d8c920bbed657f4fde61f1fbcd", + "sha256:a9d72d9abaf65628f0f31bbb573b7d9304e43b1e6bbae43149c17737a42764c4", + "sha256:ac3cf835c334fcc6b74dc4e630f9b5ff7b4c43f7fb2a7813208d95d4e10b5623", + "sha256:b091e5d4cbbe79f0e8b6b6b522346e54a282eadb06e3fd761e9b6fafc2ca91ad", + "sha256:cc070fc43a494e42732d6ae2f6621db040611c1dde64762a40c8418023af56d7", + "sha256:e1080e37c090534adb2dd7ae1c59ee883e5d8c3e63d2a4d43c20ee348d0459c5", + "sha256:f084d513de729ff10cd72a1f80db468cff464fedb1ef2fea030221a0f62d7ff4", + "sha256:f6a7421da632fc01e8a3ecd19c3f7350258d82501a646747664bae9c6a87c731" + ], + "version": "==1.18.0" }, "packaging": { "hashes": [ @@ -499,6 +521,12 @@ ], "version": "==1.9.2" }, + "pathspec": { + "hashes": [ + "sha256:e285ccc8b0785beadd4c18e5708b12bb8fcf529a1e61215b3feff1d1e559ea5c" + ], + "version": "==0.6.0" + }, "pillow": { "hashes": [ "sha256:047d9473cf68af50ac85f8ee5d5f21a60f849bc17d348da7fc85711287a75031", @@ -606,11 +634,11 @@ }, "pytest": { "hashes": [ - "sha256:63344a2e3bce2e4d522fd62b4fdebb647c019f1f9e4ca075debbd13219db4418", - "sha256:f67403f33b2b1d25a6756184077394167fe5e2f9d8bdaab30707d19ccec35427" + "sha256:6b571215b5a790f9b41f19f3531c53a45cf6bb8ef2988bc1ff9afb38270b25fa", + "sha256:e41d489ff43948babd0fad7ad5e49b8735d5d55e26628a58673c39ff61d95de4" ], "index": "pypi", - "version": "==5.3.1" + "version": "==5.3.2" }, "pytest-cov": { "hashes": [ @@ -643,19 +671,19 @@ }, "pyyaml": { "hashes": [ - "sha256:0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", - "sha256:2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", - "sha256:35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", - "sha256:38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", - "sha256:483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", - "sha256:4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", - "sha256:7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", - "sha256:8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", - "sha256:c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", - "sha256:e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", - "sha256:ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4" + "sha256:21a8e19e2007a4047ffabbd8f0ee32c0dabae3b7f4b6c645110ae53e7714b470", + "sha256:74ad685bfb065f4bdd36d24aa97092f04bcbb1179b5ffdd3d5f994023fb8c292", + "sha256:79c3ba1da22e61c2a71aaa382c57518ab492278c8974c40187b900b50f3e0282", + "sha256:94ad913ab3fd967d14ecffda8182d7d0e1f7dd919b352773c492ec51890d3224", + "sha256:998db501e3a627c3e5678d6505f0e182d1529545df289db036cdc717f35d8058", + "sha256:9b69d4645bff5820713e8912bc61c4277dc127a6f8c197b52b6436503c42600f", + "sha256:9da13b536533518343a04f3c6564782ec8a13c705310b26b4832d77fa4d92a47", + "sha256:a76159f13b47fb44fb2acac8fef798a1940dd31b4acec6f4560bd11b2d92d31b", + "sha256:a9e9175c1e47a089a2b45d9e2afc6aae1f1f725538c32eec761894a42ba1227f", + "sha256:ea51ce7b96646ecd3bb12c2702e570c2bd7dd4d9f146db7fa83c5008ede35f66", + "sha256:ffbaaa05de60fc444eda3f6300d1af27d965b09b67f1fb4ebcc88dd0fb4ab1b4" ], - "version": "==5.2" + "version": "==5.3b1" }, "readme-renderer": { "hashes": [ @@ -664,6 +692,32 @@ ], "version": "==24.0" }, + "regex": { + "hashes": [ + "sha256:032fdcc03406e1a6485ec09b826eac78732943840c4b29e503b789716f051d8d", + "sha256:0e6cf1e747f383f52a0964452658c04300a9a01e8a89c55ea22813931b580aa8", + "sha256:106e25a841921d8259dcef2a42786caae35bc750fb996f830065b3dfaa67b77e", + "sha256:1768cf42a78a11dae63152685e7a1d90af7a8d71d2d4f6d2387edea53a9e0588", + "sha256:27d1bd20d334f50b7ef078eba0f0756a640fd25f5f1708d3b5bed18a5d6bced9", + "sha256:29b20f66f2e044aafba86ecf10a84e611b4667643c42baa004247f5dfef4f90b", + "sha256:4850c78b53acf664a6578bba0e9ebeaf2807bb476c14ec7e0f936f2015133cae", + "sha256:57eacd38a5ec40ed7b19a968a9d01c0d977bda55664210be713e750dd7b33540", + "sha256:724eb24b92fc5fdc1501a1b4df44a68b9c1dda171c8ef8736799e903fb100f63", + "sha256:77ae8d926f38700432807ba293d768ba9e7652df0cbe76df2843b12f80f68885", + "sha256:78b3712ec529b2a71731fbb10b907b54d9c53a17ca589b42a578bc1e9a2c82ea", + "sha256:7bbbdbada3078dc360d4692a9b28479f569db7fc7f304b668787afc9feb38ec8", + "sha256:8d9ef7f6c403e35e73b7fc3cde9f6decdc43b1cb2ff8d058c53b9084bfcb553e", + "sha256:a83049eb717ae828ced9cf607845929efcb086a001fc8af93ff15c50012a5716", + "sha256:adc35d38952e688535980ae2109cad3a109520033642e759f987cf47fe278aa1", + "sha256:c29a77ad4463f71a506515d9ec3a899ed026b4b015bf43245c919ff36275444b", + "sha256:cfd31b3300fefa5eecb2fe596c6dee1b91b3a05ece9d5cfd2631afebf6c6fadd", + "sha256:d3ee0b035816e0520fac928de31b6572106f0d75597f6fa3206969a02baba06f", + "sha256:d508875793efdf6bab3d47850df8f40d4040ae9928d9d80864c1768d6aeaf8e3", + "sha256:ef0b828a7e22e58e06a1cceddba7b4665c6af8afeb22a0d8083001330572c147", + "sha256:faad39fdbe2c2ccda9846cd21581063086330efafa47d87afea4073a08128656" + ], + "version": "==2019.12.20" + }, "requests": { "hashes": [ "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", @@ -699,6 +753,13 @@ ], "version": "==1.2.0" }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + }, "tornado": { "hashes": [ "sha256:349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c", @@ -713,10 +774,10 @@ }, "tqdm": { "hashes": [ - "sha256:7543892c59720e36e4212180274d8f58dde36803bc1f6370fd09afa20b8f5892", - "sha256:f0ab01cf3ae5673d18f918700c0165e5fad0f26b5ebe4b34f62ead92686b5340" + "sha256:166a82cdea964ae45528e0cc89436255ff2be73dc848bdf239f13c501cae5dc7", + "sha256:9036904496bd2afacf836a6f206c5a766ce11d3e9319d54a4e794c0f34b111dc" ], - "version": "==4.40.2" + "version": "==4.41.0" }, "twine": { "hashes": [ diff --git a/annofabcli/statistics/graph.py b/annofabcli/statistics/graph.py index 3889d383..37e82f41 100644 --- a/annofabcli/statistics/graph.py +++ b/annofabcli/statistics/graph.py @@ -200,6 +200,21 @@ def write_プロジェクト全体のヒストグラム(self, df: pd.DataFrame): x_axis_label="1回目の受入時間[hour]", title="1回目の受入時間", ), + HistogramName( + column="first_annotator_worktime_hour", + x_axis_label="1回目の教師付者の作業時間[hour]", + title="1回目の教師付者の作業時間", + ), + HistogramName( + column="first_inspector_worktime_hour", + x_axis_label="1回目の検査者の作業時間[hour]", + title="1回目の検査者の作業時間", + ), + HistogramName( + column="first_acceptor_worktime_hour", + x_axis_label="1回目の受入者の作業時間[hour]", + title="1回目の受入者の作業時間", + ), HistogramName(column="inspection_count", x_axis_label="検査コメント数", title="検査コメント数"), HistogramName( column="input_data_count_of_inspection", diff --git a/annofabcli/statistics/table.py b/annofabcli/statistics/table.py index c9017b22..35bd7860 100644 --- a/annofabcli/statistics/table.py +++ b/annofabcli/statistics/table.py @@ -6,8 +6,6 @@ Dict, List, Optional, - Set, - Tuple, ) import dateutil @@ -433,6 +431,8 @@ def acceptance_is_skipped(arg_task_histories: List[TaskHistory]) -> bool: ] ) + # 最初の教師者が担当した履歴の合計作業時間を取得する。 + # 担当者変更がなければ、"annotation_worktime_hour"と"first_annotation_worktime_hour"は同じ値 task["first_annotator_worktime_hour"] = ( sum([e for e in annotation_histories if e["account_id"] == first_annotation_history["account_id"]]) if first_annotation_history is not None From ca68eacf71e52d931aacff34cd4c401db0af6d6f Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Tue, 24 Dec 2019 21:12:50 +0900 Subject: [PATCH 05/19] =?UTF-8?q?[statistics]=20=E5=BE=AE=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- annofabcli/statistics/table.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/annofabcli/statistics/table.py b/annofabcli/statistics/table.py index 35bd7860..e79f095c 100644 --- a/annofabcli/statistics/table.py +++ b/annofabcli/statistics/table.py @@ -372,6 +372,26 @@ def _set_first_phase_from_task_history( return task + def _get_first_operator_worktime(self, task_histories: List[TaskHistory], account_id: str) -> float: + """ + 対象ユーザが担当したフェーズの合計作業時間を取得する。 + + Args: + task_histories: + account_id: 対象】ユーザのaccount_id + + Returns: + 作業時間 + + """ + return sum( + [ + isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) + for e in task_histories + if e["account_id"] == account_id + ] + ) + def set_task_histories(self, task: Task, task_histories: List[TaskHistory]): """ タスク履歴関係の情報を設定する @@ -434,17 +454,18 @@ def acceptance_is_skipped(arg_task_histories: List[TaskHistory]) -> bool: # 最初の教師者が担当した履歴の合計作業時間を取得する。 # 担当者変更がなければ、"annotation_worktime_hour"と"first_annotation_worktime_hour"は同じ値 task["first_annotator_worktime_hour"] = ( - sum([e for e in annotation_histories if e["account_id"] == first_annotation_history["account_id"]]) + self._get_first_operator_worktime(annotation_histories, first_annotation_history["account_id"]) if first_annotation_history is not None else 0 ) + task["first_inspector_worktime_hour"] = ( - sum([e for e in inspection_histories if e["account_id"] == first_inspection_history["account_id"]]) + self._get_first_operator_worktime(inspection_histories, first_inspection_history["account_id"]) if first_inspection_history is not None else 0 ) task["first_acceptor_worktime_hour"] = ( - sum([e for e in acceptance_histories if e["account_id"] == first_acceptance_history["account_id"]]) + self._get_first_operator_worktime(acceptance_histories, first_acceptance_history["account_id"]) if first_acceptance_history is not None else 0 ) From 87274da96f0e8737fb94e017c3c5ae66386e9533 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Tue, 24 Dec 2019 21:25:13 +0900 Subject: [PATCH 06/19] modify --- annofabcli/statistics/graph.py | 4 +++- annofabcli/statistics/table.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/annofabcli/statistics/graph.py b/annofabcli/statistics/graph.py index 37e82f41..dcb5c8a1 100644 --- a/annofabcli/statistics/graph.py +++ b/annofabcli/statistics/graph.py @@ -1,3 +1,4 @@ +# pylint: disable=too-many-lines import logging from dataclasses import dataclass from pathlib import Path @@ -125,7 +126,8 @@ def _set_legend(fig: bokeh.plotting.Figure, hover_tool: HoverTool): legend = fig.legend[0] fig.add_layout(legend, "left") - def _create_histogram(self, df: pd.DataFrame, histogram_name: HistogramName, bins: int = 20) -> hv.Histogram: + @staticmethod + def _create_histogram(df: pd.DataFrame, histogram_name: HistogramName, bins: int = 20) -> hv.Histogram: """ ヒストグラムを生成する。 Args: diff --git a/annofabcli/statistics/table.py b/annofabcli/statistics/table.py index e79f095c..b3252977 100644 --- a/annofabcli/statistics/table.py +++ b/annofabcli/statistics/table.py @@ -372,7 +372,8 @@ def _set_first_phase_from_task_history( return task - def _get_first_operator_worktime(self, task_histories: List[TaskHistory], account_id: str) -> float: + @staticmethod + def _get_first_operator_worktime(task_histories: List[TaskHistory], account_id: str) -> float: """ 対象ユーザが担当したフェーズの合計作業時間を取得する。 From aae0e0bf7e5c7659587ef8caa23db4eb282d77d0 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Tue, 24 Dec 2019 21:42:20 +0900 Subject: [PATCH 07/19] update graph title --- annofabcli/statistics/graph.py | 5 +- annofabcli/statistics/visualize_statistics.py | 50 +++++++++---------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/annofabcli/statistics/graph.py b/annofabcli/statistics/graph.py index dcb5c8a1..3ba47c6d 100644 --- a/annofabcli/statistics/graph.py +++ b/annofabcli/statistics/graph.py @@ -149,8 +149,7 @@ def _create_histogram(df: pd.DataFrame, histogram_name: HistogramName, bins: int (edges, frequencies), kdims=histogram_name.x_axis_label, vdims=histogram_name.y_axis_label, - label=title, - ).options(width=500) + ).options(width=500, title=title, fontsize={"title": 10}) return hist def write_プロジェクト全体のヒストグラム(self, df: pd.DataFrame): @@ -271,7 +270,7 @@ def write_プロジェクト全体のヒストグラム(self, df: pd.DataFrame): )) # 軸範囲が同期しないようにする - layout = hv.Layout(histograms).options(shared_axes=False) + layout = hv.Layout(histograms).options(shared_axes=False).cols(3) renderer.save(layout, f"{self.outdir}/html/{self.short_project_id}-ヒストグラム-プロジェクト全体") def wirte_ラベルごとのアノテーション数(self, df: pd.DataFrame): diff --git a/annofabcli/statistics/visualize_statistics.py b/annofabcli/statistics/visualize_statistics.py index 5631c488..baeda82a 100644 --- a/annofabcli/statistics/visualize_statistics.py +++ b/annofabcli/statistics/visualize_statistics.py @@ -87,31 +87,31 @@ def visualize_statistics( task_cumulative_df_by_inspector = table_obj.create_cumulative_df_by_first_inspector(task_df) task_cumulative_df_by_acceptor = table_obj.create_cumulative_df_by_first_acceptor(task_df) - try: - tsv_obj.write_task_list(task_df, dropped_columns=["histories_by_phase", "input_data_id_list"]) - tsv_obj.write_task_history_list(task_history_df) - tsv_obj.write_inspection_list(df=inspection_df, dropped_columns=["data"], only_error_corrected=True) - tsv_obj.write_inspection_list( - df=inspection_df_all, - dropped_columns=["data"], - only_error_corrected=False, - ) - - tsv_obj.write_member_list(member_df) - tsv_obj.write_ラベルごとのアノテーション数(annotation_df) - - tsv_obj.write_教師付作業者別日毎の情報(by_date_df) - tsv_obj.write_ユーザ別日毎の作業時間(table_obj.create_account_statistics_df()) - - for phase in TaskPhase: - df_by_inputs = table_obj.create_worktime_per_image_df(AggregationBy.BY_INPUTS, phase) - tsv_obj.write_メンバー別作業時間平均_画像1枚あたり(df_by_inputs, phase) - df_by_tasks = table_obj.create_worktime_per_image_df(AggregationBy.BY_TASKS, phase) - tsv_obj.write_メンバー別作業時間平均_タスク1個あたり(df_by_tasks, phase) - - except Exception as e: # pylint: disable=broad-except - logger.warning(e) - logger.exception(e) + # try: + # tsv_obj.write_task_list(task_df, dropped_columns=["histories_by_phase", "input_data_id_list"]) + # tsv_obj.write_task_history_list(task_history_df) + # tsv_obj.write_inspection_list(df=inspection_df, dropped_columns=["data"], only_error_corrected=True) + # tsv_obj.write_inspection_list( + # df=inspection_df_all, + # dropped_columns=["data"], + # only_error_corrected=False, + # ) + # + # tsv_obj.write_member_list(member_df) + # tsv_obj.write_ラベルごとのアノテーション数(annotation_df) + # + # tsv_obj.write_教師付作業者別日毎の情報(by_date_df) + # tsv_obj.write_ユーザ別日毎の作業時間(table_obj.create_account_statistics_df()) + # + # for phase in TaskPhase: + # df_by_inputs = table_obj.create_worktime_per_image_df(AggregationBy.BY_INPUTS, phase) + # tsv_obj.write_メンバー別作業時間平均_画像1枚あたり(df_by_inputs, phase) + # df_by_tasks = table_obj.create_worktime_per_image_df(AggregationBy.BY_TASKS, phase) + # tsv_obj.write_メンバー別作業時間平均_タスク1個あたり(df_by_tasks, phase) + # + # except Exception as e: # pylint: disable=broad-except + # logger.warning(e) + # logger.exception(e) try: graph_obj.wirte_ラベルごとのアノテーション数(annotation_df) From 5d88db31b81a3bb2a0d6da2f854e34490c02b933 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Tue, 24 Dec 2019 22:01:29 +0900 Subject: [PATCH 08/19] =?UTF-8?q?=E3=83=92=E3=82=B9=E3=83=88=E3=82=B0?= =?UTF-8?q?=E3=83=A9=E3=83=A0=E3=82=92=E5=88=86=E5=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- annofabcli/statistics/graph.py | 71 +++++++++++++------ annofabcli/statistics/visualize_statistics.py | 53 +++++++------- 2 files changed, 77 insertions(+), 47 deletions(-) diff --git a/annofabcli/statistics/graph.py b/annofabcli/statistics/graph.py index 3ba47c6d..4ebcc6a8 100644 --- a/annofabcli/statistics/graph.py +++ b/annofabcli/statistics/graph.py @@ -152,13 +152,13 @@ def _create_histogram(df: pd.DataFrame, histogram_name: HistogramName, bins: int ).options(width=500, title=title, fontsize={"title": 10}) return hist - def write_プロジェクト全体のヒストグラム(self, df: pd.DataFrame): + + def write_histogram_for_worktime(self, df: pd.DataFrame): """ - 以下の情報をヒストグラムで出力する。 - 作業時間関係、検査コメント数関係、アノテーション数 + 作業時間に関する情報をヒストグラムとして表示する。 Args: - df: タスク一覧のDataFrame. Noneならば新たに生成する + df: タスク一覧のDataFrame """ if len(df) == 0: @@ -168,9 +168,6 @@ def write_プロジェクト全体のヒストグラム(self, df: pd.DataFrame): renderer = hv.renderer("bokeh") histogram_name_list = [ - HistogramName(column="annotation_count", x_axis_label="アノテーション数", title="アノテーション数"), - HistogramName(column="input_data_count", x_axis_label="画像枚数", title="画像枚数"), - HistogramName(column="sum_worktime_hour", x_axis_label="総作業時間[hour]", title="総作業時間"), HistogramName( column="annotation_worktime_hour", x_axis_label="教師付時間[hour]", @@ -216,13 +213,57 @@ def write_プロジェクト全体のヒストグラム(self, df: pd.DataFrame): x_axis_label="1回目の受入者の作業時間[hour]", title="1回目の受入者の作業時間", ), + HistogramName(column="sum_worktime_hour", x_axis_label="総作業時間[hour]", title="総作業時間"), + ] + + histograms = [] + for histogram_name in histogram_name_list: + filtered_df = df[df[histogram_name.column].notnull()] + hist = self._create_histogram(filtered_df, histogram_name=histogram_name) + histograms.append(hist) + + # 自動受入したタスクを除外して、受入時間をグラフ化する + filtered_df = df[df["acceptance_worktime_hour"].notnull()] + histograms.append( + self._create_histogram( + filtered_df[~ filtered_df["acceptance_is_skipped"]], + histogram_name=HistogramName( + column="acceptance_worktime_hour", + x_axis_label="受入時間[hour]", + title="受入時間(自動受入されたタスクを除外)", + ), + )) + + # 軸範囲が同期しないようにする + layout = hv.Layout(histograms).options(shared_axes=False).cols(3) + renderer.save(layout, f"{self.outdir}/html/{self.short_project_id}-ヒストグラム-作業時間") + + + def write_histogram_for_other(self, df: pd.DataFrame): + """ + アノテーション数や、検査コメント数など、作業時間以外の情報をヒストグラムで表示する。 + + Args: + df: タスク一覧のDataFrame + + """ + if len(df) == 0: + logger.info("タスク一覧が0件のため出力しない") + return + + renderer = hv.renderer("bokeh") + + histogram_name_list = [ + HistogramName(column="annotation_count", x_axis_label="アノテーション数", title="アノテーション数"), + HistogramName(column="input_data_count", x_axis_label="画像枚数", title="画像枚数"), HistogramName(column="inspection_count", x_axis_label="検査コメント数", title="検査コメント数"), HistogramName( column="input_data_count_of_inspection", x_axis_label="指摘を受けた画像枚数", title="指摘を受けた画像枚数", ), - # 経過時間 + + # 経過日数 HistogramName( column="diff_days_to_first_inspection_started", x_axis_label="最初の検査を着手するまでの日数", @@ -257,21 +298,9 @@ def write_プロジェクト全体のヒストグラム(self, df: pd.DataFrame): hist = self._create_histogram(filtered_df, histogram_name=histogram_name) histograms.append(hist) - # 自動受入したタスクを除外して、受入時間をグラフ化する - filtered_df = df[df["acceptance_worktime_hour"].notnull()] - histograms.append( - self._create_histogram( - filtered_df[~ filtered_df["acceptance_is_skipped"]], - histogram_name=HistogramName( - column="acceptance_worktime_hour", - x_axis_label="受入時間[hour]", - title="受入時間(自動受入タスクを除外)", - ), - )) - # 軸範囲が同期しないようにする layout = hv.Layout(histograms).options(shared_axes=False).cols(3) - renderer.save(layout, f"{self.outdir}/html/{self.short_project_id}-ヒストグラム-プロジェクト全体") + renderer.save(layout, f"{self.outdir}/html/{self.short_project_id}-ヒストグラム") def wirte_ラベルごとのアノテーション数(self, df: pd.DataFrame): """ diff --git a/annofabcli/statistics/visualize_statistics.py b/annofabcli/statistics/visualize_statistics.py index baeda82a..70475196 100644 --- a/annofabcli/statistics/visualize_statistics.py +++ b/annofabcli/statistics/visualize_statistics.py @@ -87,35 +87,36 @@ def visualize_statistics( task_cumulative_df_by_inspector = table_obj.create_cumulative_df_by_first_inspector(task_df) task_cumulative_df_by_acceptor = table_obj.create_cumulative_df_by_first_acceptor(task_df) - # try: - # tsv_obj.write_task_list(task_df, dropped_columns=["histories_by_phase", "input_data_id_list"]) - # tsv_obj.write_task_history_list(task_history_df) - # tsv_obj.write_inspection_list(df=inspection_df, dropped_columns=["data"], only_error_corrected=True) - # tsv_obj.write_inspection_list( - # df=inspection_df_all, - # dropped_columns=["data"], - # only_error_corrected=False, - # ) - # - # tsv_obj.write_member_list(member_df) - # tsv_obj.write_ラベルごとのアノテーション数(annotation_df) - # - # tsv_obj.write_教師付作業者別日毎の情報(by_date_df) - # tsv_obj.write_ユーザ別日毎の作業時間(table_obj.create_account_statistics_df()) - # - # for phase in TaskPhase: - # df_by_inputs = table_obj.create_worktime_per_image_df(AggregationBy.BY_INPUTS, phase) - # tsv_obj.write_メンバー別作業時間平均_画像1枚あたり(df_by_inputs, phase) - # df_by_tasks = table_obj.create_worktime_per_image_df(AggregationBy.BY_TASKS, phase) - # tsv_obj.write_メンバー別作業時間平均_タスク1個あたり(df_by_tasks, phase) - # - # except Exception as e: # pylint: disable=broad-except - # logger.warning(e) - # logger.exception(e) + try: + tsv_obj.write_task_list(task_df, dropped_columns=["histories_by_phase", "input_data_id_list"]) + tsv_obj.write_task_history_list(task_history_df) + tsv_obj.write_inspection_list(df=inspection_df, dropped_columns=["data"], only_error_corrected=True) + tsv_obj.write_inspection_list( + df=inspection_df_all, + dropped_columns=["data"], + only_error_corrected=False, + ) + + tsv_obj.write_member_list(member_df) + tsv_obj.write_ラベルごとのアノテーション数(annotation_df) + + tsv_obj.write_教師付作業者別日毎の情報(by_date_df) + tsv_obj.write_ユーザ別日毎の作業時間(table_obj.create_account_statistics_df()) + + for phase in TaskPhase: + df_by_inputs = table_obj.create_worktime_per_image_df(AggregationBy.BY_INPUTS, phase) + tsv_obj.write_メンバー別作業時間平均_画像1枚あたり(df_by_inputs, phase) + df_by_tasks = table_obj.create_worktime_per_image_df(AggregationBy.BY_TASKS, phase) + tsv_obj.write_メンバー別作業時間平均_タスク1個あたり(df_by_tasks, phase) + + except Exception as e: # pylint: disable=broad-except + logger.warning(e) + logger.exception(e) try: graph_obj.wirte_ラベルごとのアノテーション数(annotation_df) - graph_obj.write_プロジェクト全体のヒストグラム(task_df) + graph_obj.write_histogram_for_worktime(task_df) + graph_obj.write_histogram_for_other(task_df) graph_obj.write_cumulative_line_graph_for_annotator( df=task_cumulative_df_by_annotator, first_annotation_user_id_list=user_id_list, From 1fd6ec2f2d3128f03b58063bda79deae248b9b7e Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Tue, 24 Dec 2019 22:05:45 +0900 Subject: [PATCH 09/19] =?UTF-8?q?=E3=83=95=E3=82=A9=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=82=B5=E3=82=A4=E3=82=BA=E3=82=92=E8=AA=BF=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- annofabcli/statistics/graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/annofabcli/statistics/graph.py b/annofabcli/statistics/graph.py index 4ebcc6a8..d2df3d51 100644 --- a/annofabcli/statistics/graph.py +++ b/annofabcli/statistics/graph.py @@ -149,7 +149,7 @@ def _create_histogram(df: pd.DataFrame, histogram_name: HistogramName, bins: int (edges, frequencies), kdims=histogram_name.x_axis_label, vdims=histogram_name.y_axis_label, - ).options(width=500, title=title, fontsize={"title": 10}) + ).options(width=500, title=title, fontsize={"title": 9}) return hist From 4a13aee07f64cfcec4681a7c4cadf41f3292d935 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Tue, 24 Dec 2019 22:09:23 +0900 Subject: [PATCH 10/19] =?UTF-8?q?tsv.py:=20=E8=A1=A8=E7=A4=BA=E3=81=99?= =?UTF-8?q?=E3=82=8B=E5=88=97=E9=A0=86=E3=82=92=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- annofabcli/statistics/tsv.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/annofabcli/statistics/tsv.py b/annofabcli/statistics/tsv.py index fd3dd4cf..85684514 100755 --- a/annofabcli/statistics/tsv.py +++ b/annofabcli/statistics/tsv.py @@ -118,15 +118,29 @@ def write_task_list(self, df: pd.DataFrame, dropped_columns: List[str] = None): "started_datetime", "updated_datetime", "sampling", - # 最初のアノテーション作業に関すること + # 1回目の教師付フェーズ "first_annotation_user_id", "first_annotation_worktime_hour", "first_annotation_started_datetime", + # 1回目の検査フェーズ + "first_inspection_user_id", + "first_inspection_worktime_hour", + "first_inspection_started_datetime", + # 1回目の受入フェーズ + "first_acceptance_user_id", + "first_acceptance_worktime_hour", + "first_acceptance_started_datetime", # 作業時間に関する内容 "sum_worktime_hour", "annotation_worktime_hour", "inspection_worktime_hour", "acceptance_worktime_hour", + "first_annotation_worktime_hour", + "first_inspection_worktime_hour", + "first_acceptance_worktime_hour", + "first_annotator_worktime_hour", + "first_inspector_worktime_hour", + "first_acceptor_worktime_hour", # 個数 "input_data_count", "annotation_count", From 2fe07bdd10ca235c229a4003f58c33b9c3560a47 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Fri, 27 Dec 2019 15:09:44 +0900 Subject: [PATCH 11/19] =?UTF-8?q?=E3=83=92=E3=82=B9=E3=83=88=E3=82=B0?= =?UTF-8?q?=E3=83=A9=E3=83=A0=E3=81=AE=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- annofabcli/statistics/graph.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/annofabcli/statistics/graph.py b/annofabcli/statistics/graph.py index d2df3d51..9eb42e74 100644 --- a/annofabcli/statistics/graph.py +++ b/annofabcli/statistics/graph.py @@ -302,9 +302,9 @@ def write_histogram_for_other(self, df: pd.DataFrame): layout = hv.Layout(histograms).options(shared_axes=False).cols(3) renderer.save(layout, f"{self.outdir}/html/{self.short_project_id}-ヒストグラム") - def wirte_ラベルごとのアノテーション数(self, df: pd.DataFrame): + def write_histogram_for_annotation_count_by_label(self, df: pd.DataFrame) -> None: """ - アノテーションラベルごとの個数を出力 + アノテーションラベルごとのアノテーション数をヒストグラムで出力する。 """ if len(df) == 0: logger.info("タスク一覧が0件のため出力しない") @@ -312,24 +312,19 @@ def wirte_ラベルごとのアノテーション数(self, df: pd.DataFrame): renderer = hv.renderer("bokeh") - histograms2 = [] + histograms = [] label_columns = [e for e in df.columns if e.startswith("label_")] for column in label_columns: label_name = column[len("label_"):] - mean = round(df[column].mean(), 2) - std = round(df[column].std(), 2) - title = f"{label_name}(mean = {mean}, std = {std})" - axis_name = f"アノテーション数_{label_name}" + histogram_name= HistogramName(column=column, x_axis_label=f"'{label_name}'のアノテーション数", title=f"{label_name}") + hist = self._create_histogram(df, histogram_name=histogram_name) - data = df[column].values - frequencies, edges = np.histogram(data, 20) - hist = hv.Histogram((edges, frequencies), kdims=axis_name, vdims="タスク数", label=title).options(width=400) - histograms2.append(hist) + histograms.append(hist) # 軸範囲が同期しないようにする - layout2 = hv.Layout(histograms2).options(shared_axes=False) - renderer.save(layout2, f"{self.outdir}/html/{self.short_project_id}-ヒストグラム-ラベルごとのアノテーション数") + layout = hv.Layout(histograms).options(shared_axes=False) + renderer.save(layout, f"{self.outdir}/html/{self.short_project_id}-ヒストグラム-ラベルごとのアノテーション数") def create_user_id_list( self, From c0144ce18fa6f398dce533a21b8bb4080ff15b67 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Fri, 27 Dec 2019 15:38:23 +0900 Subject: [PATCH 12/19] =?UTF-8?q?[statisitics]=20=E3=83=92=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=82=B0=E3=83=A9=E3=83=A0=E3=81=AB=E3=83=9B=E3=83=90?= =?UTF-8?q?=E3=83=BC=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- annofabcli/statistics/graph.py | 35 ++++++++++--------- annofabcli/statistics/visualize_statistics.py | 2 +- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/annofabcli/statistics/graph.py b/annofabcli/statistics/graph.py index 9eb42e74..40929131 100644 --- a/annofabcli/statistics/graph.py +++ b/annofabcli/statistics/graph.py @@ -138,8 +138,8 @@ def _create_histogram(df: pd.DataFrame, histogram_name: HistogramName, bins: int Returns: ヒストグラムオブジェクト """ - mean = round(df[histogram_name.column].mean(), 2) - std = round(df[histogram_name.column].std(), 2) + mean = round(df[histogram_name.column].mean(), 3) + std = round(df[histogram_name.column].std(), 3) title = f"{histogram_name.title}: μ={mean}, α={std}, N={len(df)}" data = df[histogram_name.column].values @@ -149,6 +149,7 @@ def _create_histogram(df: pd.DataFrame, histogram_name: HistogramName, bins: int (edges, frequencies), kdims=histogram_name.x_axis_label, vdims=histogram_name.y_axis_label, + tools=["hover"] ).options(width=500, title=title, fontsize={"title": 9}) return hist @@ -183,21 +184,6 @@ def write_histogram_for_worktime(self, df: pd.DataFrame): x_axis_label="受入時間[hour]", title="受入時間", ), - HistogramName( - column="first_annotation_worktime_hour", - x_axis_label="1回目の教師付時間[hour]", - title="1回目の教師付時間", - ), - HistogramName( - column="first_inspection_worktime_hour", - x_axis_label="1回目の検査時間[hour]", - title="1回目の検査時間", - ), - HistogramName( - column="first_acceptance_worktime_hour", - x_axis_label="1回目の受入時間[hour]", - title="1回目の受入時間", - ), HistogramName( column="first_annotator_worktime_hour", x_axis_label="1回目の教師付者の作業時間[hour]", @@ -213,6 +199,21 @@ def write_histogram_for_worktime(self, df: pd.DataFrame): x_axis_label="1回目の受入者の作業時間[hour]", title="1回目の受入者の作業時間", ), + HistogramName( + column="first_annotation_worktime_hour", + x_axis_label="1回目の教師付時間[hour]", + title="1回目の教師付時間", + ), + HistogramName( + column="first_inspection_worktime_hour", + x_axis_label="1回目の検査時間[hour]", + title="1回目の検査時間", + ), + HistogramName( + column="first_acceptance_worktime_hour", + x_axis_label="1回目の受入時間[hour]", + title="1回目の受入時間", + ), HistogramName(column="sum_worktime_hour", x_axis_label="総作業時間[hour]", title="総作業時間"), ] diff --git a/annofabcli/statistics/visualize_statistics.py b/annofabcli/statistics/visualize_statistics.py index 70475196..aabf1d5e 100644 --- a/annofabcli/statistics/visualize_statistics.py +++ b/annofabcli/statistics/visualize_statistics.py @@ -114,7 +114,7 @@ def visualize_statistics( logger.exception(e) try: - graph_obj.wirte_ラベルごとのアノテーション数(annotation_df) + graph_obj.write_histogram_for_annotation_count_by_label(annotation_df) graph_obj.write_histogram_for_worktime(task_df) graph_obj.write_histogram_for_other(task_df) graph_obj.write_cumulative_line_graph_for_annotator( From 408d406b267f3efbd4cfc104f4d81d5d0446dba9 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Fri, 27 Dec 2019 16:19:22 +0900 Subject: [PATCH 13/19] =?UTF-8?q?[statisitics]=20=E3=83=92=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=82=B0=E3=83=A9=E3=83=A0=E3=81=AB=E3=83=9B=E3=83=90?= =?UTF-8?q?=E3=83=BC=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- annofabcli/statistics/graph.py | 19 +-- annofabcli/statistics/table.py | 279 +++++++++++++-------------------- 2 files changed, 117 insertions(+), 181 deletions(-) diff --git a/annofabcli/statistics/graph.py b/annofabcli/statistics/graph.py index 40929131..73eca71e 100644 --- a/annofabcli/statistics/graph.py +++ b/annofabcli/statistics/graph.py @@ -145,15 +145,12 @@ def _create_histogram(df: pd.DataFrame, histogram_name: HistogramName, bins: int data = df[histogram_name.column].values frequencies, edges = np.histogram(data, bins) - hist = hv.Histogram( - (edges, frequencies), - kdims=histogram_name.x_axis_label, - vdims=histogram_name.y_axis_label, - tools=["hover"] - ).options(width=500, title=title, fontsize={"title": 9}) + hist = hv.Histogram((edges, frequencies), kdims=histogram_name.x_axis_label, + vdims=histogram_name.y_axis_label).options(width=500, title=title, fontsize={ + "title": 9 + }).opts(hv.opts(tools=["hover"])) return hist - def write_histogram_for_worktime(self, df: pd.DataFrame): """ 作業時間に関する情報をヒストグラムとして表示する。 @@ -227,7 +224,7 @@ def write_histogram_for_worktime(self, df: pd.DataFrame): filtered_df = df[df["acceptance_worktime_hour"].notnull()] histograms.append( self._create_histogram( - filtered_df[~ filtered_df["acceptance_is_skipped"]], + filtered_df[~filtered_df["acceptance_is_skipped"]], histogram_name=HistogramName( column="acceptance_worktime_hour", x_axis_label="受入時間[hour]", @@ -239,7 +236,6 @@ def write_histogram_for_worktime(self, df: pd.DataFrame): layout = hv.Layout(histograms).options(shared_axes=False).cols(3) renderer.save(layout, f"{self.outdir}/html/{self.short_project_id}-ヒストグラム-作業時間") - def write_histogram_for_other(self, df: pd.DataFrame): """ アノテーション数や、検査コメント数など、作業時間以外の情報をヒストグラムで表示する。 @@ -318,13 +314,14 @@ def write_histogram_for_annotation_count_by_label(self, df: pd.DataFrame) -> Non for column in label_columns: label_name = column[len("label_"):] - histogram_name= HistogramName(column=column, x_axis_label=f"'{label_name}'のアノテーション数", title=f"{label_name}") + histogram_name = HistogramName(column=column, x_axis_label=f"'{label_name}'のアノテーション数", + title=f"{label_name}") hist = self._create_histogram(df, histogram_name=histogram_name) histograms.append(hist) # 軸範囲が同期しないようにする - layout = hv.Layout(histograms).options(shared_axes=False) + layout = hv.Layout(histograms).options(shared_axes=False).cols(3) renderer.save(layout, f"{self.outdir}/html/{self.short_project_id}-ヒストグラム-ラベルごとのアノテーション数") def create_user_id_list( diff --git a/annofabcli/statistics/table.py b/annofabcli/statistics/table.py index b3252977..5ffd4b2c 100644 --- a/annofabcli/statistics/table.py +++ b/annofabcli/statistics/table.py @@ -1,31 +1,14 @@ import copy import logging from enum import Enum -from typing import ( - Any, - Dict, - List, - Optional, -) +from typing import Any, Dict, List, Optional import dateutil import pandas as pd from annofabapi.dataclass.annotation import SimpleAnnotationDetail -from annofabapi.dataclass.statistics import ( - ProjectAccountStatistics, - ProjectAccountStatisticsHistory, - WorktimeStatistics, - WorktimeStatisticsItem, -) -from annofabapi.models import ( - InputDataId, - Inspection, - InspectionStatus, - Task, - TaskHistory, - TaskPhase, - TaskStatus, -) +from annofabapi.dataclass.statistics import (ProjectAccountStatistics, ProjectAccountStatisticsHistory, + WorktimeStatistics, WorktimeStatisticsItem) +from annofabapi.models import InputDataId, Inspection, InspectionStatus, Task, TaskHistory, TaskPhase, TaskStatus from more_itertools import first_true import annofabcli @@ -71,7 +54,10 @@ class Table: _account_statistics: Optional[List[ProjectAccountStatistics]] = None def __init__( - self, database: Database, task_query_param: Dict[str, Any], ignored_task_id_list: Optional[List[str]] = None, + self, + database: Database, + task_query_param: Dict[str, Any], + ignored_task_id_list: Optional[List[str]] = None, ): self.annofab_service = database.annofab_service self.annofab_facade = AnnofabApiFacade(database.annofab_service) @@ -332,9 +318,8 @@ def get_rejections_by_phase(task_histories: List[TaskHistory], phase: TaskPhase) return rejections_by_phase - def _set_first_phase_from_task_history( - self, task: Task, task_history: Optional[TaskHistory], column_prefix: str - ) -> Task: + def _set_first_phase_from_task_history(self, task: Task, task_history: Optional[TaskHistory], + column_prefix: str) -> Task: """ 最初のフェーズに関する情報をTask情報に設定する。 @@ -347,28 +332,23 @@ def _set_first_phase_from_task_history( """ if task_history is None: - task.update( - { - f"{column_prefix}_account_id": None, - f"{column_prefix}_user_id": None, - f"{column_prefix}_username": None, - f"{column_prefix}_started_datetime": None, - f"{column_prefix}_worktime_hour": 0, - } - ) + task.update({ + f"{column_prefix}_account_id": None, + f"{column_prefix}_user_id": None, + f"{column_prefix}_username": None, + f"{column_prefix}_started_datetime": None, + f"{column_prefix}_worktime_hour": 0, + }) else: account_id = task_history["account_id"] - task.update( - { - f"{column_prefix}_account_id": account_id, - f"{column_prefix}_user_id": self._get_user_id(account_id), - f"{column_prefix}_username": self._get_username(account_id), - f"{column_prefix}_started_datetime": task_history["started_datetime"], - f"{column_prefix}_worktime_hour": isoduration_to_hour( - task_history["accumulated_labor_time_milliseconds"] - ), - } - ) + task.update({ + f"{column_prefix}_account_id": account_id, + f"{column_prefix}_user_id": self._get_user_id(account_id), + f"{column_prefix}_username": self._get_username(account_id), + f"{column_prefix}_started_datetime": task_history["started_datetime"], + f"{column_prefix}_worktime_hour": isoduration_to_hour( + task_history["accumulated_labor_time_milliseconds"]), + }) return task @@ -385,19 +365,16 @@ def _get_first_operator_worktime(task_histories: List[TaskHistory], account_id: 作業時間 """ - return sum( - [ - isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) - for e in task_histories - if e["account_id"] == account_id - ] - ) + return sum([ + isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) + for e in task_histories + if e["account_id"] == account_id + ]) def set_task_histories(self, task: Task, task_histories: List[TaskHistory]): """ タスク履歴関係の情報を設定する """ - def diff_days(ended_key: str, started_key: str) -> Optional[float]: if task[ended_key] is not None and task[started_key] is not None: delta = dateutil.parser.parse(task[ended_key]) - dateutil.parser.parse(task[started_key]) @@ -407,13 +384,9 @@ def diff_days(ended_key: str, started_key: str) -> Optional[float]: def acceptance_is_skipped(arg_task_histories: List[TaskHistory]) -> bool: skipped_histories = [ - e - for e in arg_task_histories - if ( - e["phase"] == TaskPhase.ACCEPTANCE.value - and e["account_id"] is None - and annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) == 0 - ) + e for e in arg_task_histories + if (e["phase"] == TaskPhase.ACCEPTANCE.value and e["account_id"] is None + and annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) == 0) ] return len(skipped_histories) > 0 @@ -433,47 +406,31 @@ def acceptance_is_skipped(arg_task_histories: List[TaskHistory]) -> bool: first_acceptance_history = acceptance_histories[0] if len(acceptance_histories) > 0 else None self._set_first_phase_from_task_history(task, first_acceptance_history, column_prefix="first_acceptance") - task["annotation_worktime_hour"] = sum( - [ - annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) - for e in annotation_histories - ] - ) - task["inspection_worktime_hour"] = sum( - [ - annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) - for e in inspection_histories - ] - ) - task["acceptance_worktime_hour"] = sum( - [ - annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) - for e in acceptance_histories - ] - ) + task["annotation_worktime_hour"] = sum([ + annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) for e in annotation_histories + ]) + task["inspection_worktime_hour"] = sum([ + annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) for e in inspection_histories + ]) + task["acceptance_worktime_hour"] = sum([ + annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) for e in acceptance_histories + ]) # 最初の教師者が担当した履歴の合計作業時間を取得する。 # 担当者変更がなければ、"annotation_worktime_hour"と"first_annotation_worktime_hour"は同じ値 - task["first_annotator_worktime_hour"] = ( - self._get_first_operator_worktime(annotation_histories, first_annotation_history["account_id"]) - if first_annotation_history is not None - else 0 - ) + task["first_annotator_worktime_hour"] = (self._get_first_operator_worktime( + annotation_histories, first_annotation_history["account_id"]) + if first_annotation_history is not None else 0) - task["first_inspector_worktime_hour"] = ( - self._get_first_operator_worktime(inspection_histories, first_inspection_history["account_id"]) - if first_inspection_history is not None - else 0 - ) - task["first_acceptor_worktime_hour"] = ( - self._get_first_operator_worktime(acceptance_histories, first_acceptance_history["account_id"]) - if first_acceptance_history is not None - else 0 - ) + task["first_inspector_worktime_hour"] = (self._get_first_operator_worktime( + inspection_histories, first_inspection_history["account_id"]) + if first_inspection_history is not None else 0) + task["first_acceptor_worktime_hour"] = (self._get_first_operator_worktime( + acceptance_histories, first_acceptance_history["account_id"]) + if first_acceptance_history is not None else 0) task["sum_worktime_hour"] = sum( - [annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) for e in task_histories] - ) + [annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) for e in task_histories]) task["number_of_rejections_by_inspection"] = self.get_rejections_by_phase(task_histories, TaskPhase.INSPECTION) task["number_of_rejections_by_acceptance"] = self.get_rejections_by_phase(task_histories, TaskPhase.ACCEPTANCE) @@ -485,12 +442,10 @@ def acceptance_is_skipped(arg_task_histories: List[TaskHistory]) -> bool: else: task["task_completed_datetime"] = None - task["diff_days_to_first_inspection_started"] = diff_days( - "first_inspection_started_datetime", "first_annotation_started_datetime" - ) - task["diff_days_to_first_acceptance_started"] = diff_days( - "first_acceptance_started_datetime", "first_annotation_started_datetime" - ) + task["diff_days_to_first_inspection_started"] = diff_days("first_inspection_started_datetime", + "first_annotation_started_datetime") + task["diff_days_to_first_acceptance_started"] = diff_days("first_acceptance_started_datetime", + "first_annotation_started_datetime") task["diff_days_to_task_completed"] = diff_days("task_completed_datetime", "first_annotation_started_datetime") @@ -515,8 +470,7 @@ def create_task_history_df(self) -> pd.DataFrame: history["user_id"] = self._get_user_id(account_id) history["username"] = self._get_username(account_id) history["worktime_hour"] = annofabcli.utils.isoduration_to_hour( - history["accumulated_labor_time_milliseconds"] - ) + history["accumulated_labor_time_milliseconds"]) all_task_history_list.append(history) df = pd.DataFrame(all_task_history_list) @@ -529,7 +483,6 @@ def create_task_df(self) -> pd.DataFrame: first_annotation_user_id, first_annotation_started_datetime, annotation_worktime_hour, inspection_worktime_hour, acceptance_worktime_hour, sum_worktime_hour """ - def set_annotation_info(arg_task): total_annotation_count = 0 @@ -552,8 +505,7 @@ def set_inspection_info(arg_task): for inspection_list in input_data_dict.values(): # 検査コメントを絞り込む filtered_inspection_list = [ - e - for e in inspection_list + e for e in inspection_list if self._inspection_condition(e, exclude_reply=True, only_error_corrected=True) ] inspection_count += len(filtered_inspection_list) @@ -633,55 +585,45 @@ def create_dataframe_by_date(task_df: pd.DataFrame) -> pd.DataFrame: """ new_df = task_df new_df["first_annotation_started_date"] = new_df["first_annotation_started_datetime"].map( - lambda e: datetime_to_date(e) if e is not None else None - ) + lambda e: datetime_to_date(e) if e is not None else None) new_df["task_count"] = 1 # 集計用 # first_annotation_user_id と first_annotation_usernameの両方を指定している理由: # first_annotation_username を取得するため group_obj = new_df.groupby( - ["first_annotation_started_date", "first_annotation_user_id", "first_annotation_username"], as_index=False, - ) - sum_df = group_obj[ - [ - "first_annotation_worktime_hour", - "annotation_worktime_hour", - "inspection_worktime_hour", - "acceptance_worktime_hour", - "sum_worktime_hour", - "task_count", - "input_data_count", - "annotation_count", - "inspection_count", - ] - ].sum() - - sum_df["first_annotation_worktime_minute/annotation_count"] = ( - sum_df["first_annotation_worktime_hour"] * 60 / sum_df["annotation_count"] - ) - sum_df["annotation_worktime_minute/annotation_count"] = ( - sum_df["annotation_worktime_hour"] * 60 / sum_df["annotation_count"] - ) - sum_df["inspection_worktime_minute/annotation_count"] = ( - sum_df["inspection_worktime_hour"] * 60 / sum_df["annotation_count"] - ) - sum_df["acceptance_worktime_minute/annotation_count"] = ( - sum_df["acceptance_worktime_hour"] * 60 / sum_df["annotation_count"] + ["first_annotation_started_date", "first_annotation_user_id", "first_annotation_username"], + as_index=False, ) + sum_df = group_obj[[ + "first_annotation_worktime_hour", + "annotation_worktime_hour", + "inspection_worktime_hour", + "acceptance_worktime_hour", + "sum_worktime_hour", + "task_count", + "input_data_count", + "annotation_count", + "inspection_count", + ]].sum() + + sum_df["first_annotation_worktime_minute/annotation_count"] = (sum_df["first_annotation_worktime_hour"] * 60 / + sum_df["annotation_count"]) + sum_df["annotation_worktime_minute/annotation_count"] = (sum_df["annotation_worktime_hour"] * 60 / + sum_df["annotation_count"]) + sum_df["inspection_worktime_minute/annotation_count"] = (sum_df["inspection_worktime_hour"] * 60 / + sum_df["annotation_count"]) + sum_df["acceptance_worktime_minute/annotation_count"] = (sum_df["acceptance_worktime_hour"] * 60 / + sum_df["annotation_count"]) sum_df["inspection_count/annotation_count"] = sum_df["inspection_count"] / sum_df["annotation_count"] - sum_df["first_annotation_worktime_minute/input_data_count"] = ( - sum_df["first_annotation_worktime_hour"] * 60 / sum_df["input_data_count"] - ) - sum_df["annotation_worktime_minute/input_data_count"] = ( - sum_df["annotation_worktime_hour"] * 60 / sum_df["input_data_count"] - ) - sum_df["inspection_worktime_minute/input_data_count"] = ( - sum_df["inspection_worktime_hour"] * 60 / sum_df["input_data_count"] - ) - sum_df["acceptance_worktime_minute/input_data_count"] = ( - sum_df["acceptance_worktime_hour"] * 60 / sum_df["input_data_count"] - ) + sum_df["first_annotation_worktime_minute/input_data_count"] = (sum_df["first_annotation_worktime_hour"] * 60 / + sum_df["input_data_count"]) + sum_df["annotation_worktime_minute/input_data_count"] = (sum_df["annotation_worktime_hour"] * 60 / + sum_df["input_data_count"]) + sum_df["inspection_worktime_minute/input_data_count"] = (sum_df["inspection_worktime_hour"] * 60 / + sum_df["input_data_count"]) + sum_df["acceptance_worktime_minute/input_data_count"] = (sum_df["acceptance_worktime_hour"] * 60 / + sum_df["input_data_count"]) sum_df["inspection_count/input_data_count"] = sum_df["inspection_count"] / sum_df["annotation_count"] return sum_df @@ -696,19 +638,17 @@ def create_member_df(self, task_df: pd.DataFrame) -> pd.DataFrame: member_dict = {} for account_id, member in self.project_members_dict.items(): new_member = copy.deepcopy(member) - new_member.update( - { - # 初回のアノテーションに関わった個数(タスクの教師付担当者は変更されない前提) - "task_count_of_first_annotation": 0, - "input_data_count_of_first_annotation": 0, - "annotation_count_of_first_annotation": 0, - "inspection_count_of_first_annotation": 0, - # 関わった作業時間 - "annotation_worktime_hour": 0, - "inspection_worktime_hour": 0, - "acceptance_worktime_hour": 0, - } - ) + new_member.update({ + # 初回のアノテーションに関わった個数(タスクの教師付担当者は変更されない前提) + "task_count_of_first_annotation": 0, + "input_data_count_of_first_annotation": 0, + "annotation_count_of_first_annotation": 0, + "inspection_count_of_first_annotation": 0, + # 関わった作業時間 + "annotation_worktime_hour": 0, + "inspection_worktime_hour": 0, + "acceptance_worktime_hour": 0, + }) member_dict[account_id] = new_member for _, row_task in task_df.iterrows(): @@ -782,9 +722,8 @@ def create_cumulative_df_by_first_annotator(task_df: pd.DataFrame) -> pd.DataFra task_df: タスク一覧のDataFrame. 列が追加される """ # 教師付の開始時刻でソートして、indexを更新する - df = task_df.sort_values(["first_annotation_account_id", "first_annotation_started_datetime"]).reset_index( - drop=True - ) + df = task_df.sort_values(["first_annotation_account_id", + "first_annotation_started_datetime"]).reset_index(drop=True) # タスクの累計数を取得するために設定する df["task_count"] = 1 # 教師付の作業者でgroupby @@ -817,9 +756,8 @@ def create_cumulative_df_by_first_inspector(task_df: pd.DataFrame) -> pd.DataFra task_df: タスク一覧のDataFrame. 列が追加される """ - df = task_df.sort_values(["first_inspection_account_id", "first_inspection_started_datetime"]).reset_index( - drop=True - ) + df = task_df.sort_values(["first_inspection_account_id", + "first_inspection_started_datetime"]).reset_index(drop=True) # タスクの累計数を取得するために設定する df["task_count"] = 1 # 教師付の作業者でgroupby @@ -852,9 +790,8 @@ def create_cumulative_df_by_first_acceptor(task_df: pd.DataFrame) -> pd.DataFram task_df: タスク一覧のDataFrame. 列が追加される """ - df = task_df.sort_values(["first_acceptance_account_id", "first_acceptance_started_datetime"]).reset_index( - drop=True - ) + df = task_df.sort_values(["first_acceptance_account_id", + "first_acceptance_started_datetime"]).reset_index(drop=True) # タスクの累計数を取得するために設定する df["task_count"] = 1 groupby_obj = df.groupby("first_acceptance_account_id") @@ -907,6 +844,8 @@ def create_worktime_per_image_df(self, aggregation_by: AggregationBy, phase: Tas df = pd.DataFrame(worktime_info_list) # acount_idをusernameに変更する columns = { - col: self._get_username(col) for col in df.columns if col != "date" # pylint: disable=not-an-iterable + col: self._get_username(col) + for col in df.columns + if col != "date" # pylint: disable=not-an-iterable } return df.rename(columns=columns).fillna(0) From b37d62e7c4a3775bb910889491b4e20538823d44 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Fri, 27 Dec 2019 17:47:34 +0900 Subject: [PATCH 14/19] =?UTF-8?q?[format]black=E3=81=A7format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 7 +- annofabcli/__main__.py | 6 +- annofabcli/__version__.py | 2 +- .../annotation/list_annotation_count.py | 138 +++-- .../annotation/subcommand_annotation.py | 2 +- .../list_annotation_specs_history.py | 8 +- .../list_annotation_specs_label.py | 121 +++-- .../annotation_specs/print_label_color.py | 24 +- .../subcommand_annotation_specs.py | 2 +- annofabcli/common/cli.py | 97 ++-- annofabcli/common/dataclasses.py | 1 + annofabcli/common/enums.py | 16 +- annofabcli/common/exceptions.py | 3 + annofabcli/common/facade.py | 34 +- annofabcli/common/image.py | 54 +- annofabcli/common/utils.py | 36 +- annofabcli/common/visualize.py | 88 ++-- ...cated_register_annotation_from_full_zip.py | 127 +++-- .../experimental/list_labor_worktime.py | 103 ++-- .../experimental/subcommand_experimental.py | 2 +- annofabcli/experimental/utils.py | 25 +- .../filesystem/write_annotation_image.py | 66 ++- annofabcli/input_data/delete_input_data.py | 29 +- annofabcli/input_data/list_input_data.py | 138 +++-- annofabcli/input_data/put_input_data.py | 170 +++--- .../input_data/subcommand_input_data.py | 2 +- .../inspection_comment/list_inspections.py | 73 ++- .../list_unprocessed_inspections.py | 46 +- .../subcommand_inspection_comment.py | 2 +- .../instruction/subcommand_instruction.py | 2 +- annofabcli/instruction/upload_instruction.py | 25 +- annofabcli/job/list_job.py | 15 +- annofabcli/job/list_last_job.py | 39 +- annofabcli/job/subcommand_job.py | 2 +- annofabcli/labor/list_worktime_by_user.py | 189 ++++--- annofabcli/labor/subcommand_labor.py | 2 +- .../list_organization_member.py | 8 +- .../subcommand_organization_member.py | 2 +- annofabcli/project/copy_project.py | 100 ++-- annofabcli/project/diff_projects.py | 87 ++-- annofabcli/project/download.py | 61 ++- annofabcli/project/list_project.py | 47 +- annofabcli/project/subcommand_project.py | 2 +- .../project_member/change_project_members.py | 59 ++- .../project_member/copy_project_members.py | 25 +- annofabcli/project_member/delete_users.py | 35 +- annofabcli/project_member/invite_users.py | 43 +- annofabcli/project_member/list_users.py | 77 +-- .../project_member/put_project_members.py | 85 +-- .../subcommand_project_member.py | 2 +- annofabcli/statistics/database.py | 57 +- annofabcli/statistics/graph.py | 216 +++----- annofabcli/statistics/table.py | 262 ++++++---- annofabcli/statistics/tsv.py | 5 +- annofabcli/statistics/visualize_statistics.py | 48 +- .../supplementary/list_supplementary_data.py | 13 +- .../supplementary/subcommand_supplementary.py | 2 +- annofabcli/task/cancel_acceptance.py | 21 +- annofabcli/task/change_operator.py | 19 +- annofabcli/task/complete_tasks.py | 80 ++- annofabcli/task/delete_tasks.py | 11 +- annofabcli/task/list_tasks.py | 83 +-- annofabcli/task/reject_tasks.py | 113 ++-- annofabcli/task/subcommand_task.py | 2 +- tests/test_local_cli.py | 4 +- tests/test_local_complete_task.py | 56 +- tests/test_local_image.py | 33 +- tests/test_local_input_data.py | 27 +- tests/test_local_project_member.py | 23 +- tests/test_local_statistics.py | 4 +- tests/test_main.py | 492 ++++++++++++------ 71 files changed, 2366 insertions(+), 1534 deletions(-) diff --git a/Makefile b/Makefile index 67a45b29..f6a2d757 100644 --- a/Makefile +++ b/Makefile @@ -12,9 +12,10 @@ init: pipenv install --dev format: - pipenv run autoflake --in-place --remove-all-unused-imports --ignore-init-module-imports --recursive ${FORMAT_FILES} - pipenv run isort --verbose --recursive ${FORMAT_FILES} - pipenv run yapf --verbose --in-place --recursive ${FORMAT_FILES} + pipenv run black ${FORMAT_FILES} +# pipenv run autoflake --in-place --remove-all-unused-imports --ignore-init-module-imports --recursive ${FORMAT_FILES} +# pipenv run isort --verbose --recursive ${FORMAT_FILES} + #pipenv run yapf --verbose --in-place --recursive ${FORMAT_FILES} lint: pipenv run mypy ${LINT_FILES} --config-file setup.cfg diff --git a/annofabcli/__main__.py b/annofabcli/__main__.py index f1c04138..9d0a645d 100644 --- a/annofabcli/__main__.py +++ b/annofabcli/__main__.py @@ -32,10 +32,10 @@ def main(arguments: Optional[Sequence[str]] = None): """ parser = argparse.ArgumentParser(description="annofabapiを使ったCLIツール") - parser.add_argument('--version', action='version', version=f'annofabcli {annofabcli.__version__}') + parser.add_argument("--version", action="version", version=f"annofabcli {annofabcli.__version__}") parser.set_defaults(command_help=parser.print_help) - subparsers = parser.add_subparsers(dest='command_name') + subparsers = parser.add_subparsers(dest="command_name") annofabcli.annotation.subcommand_annotation.add_parser(subparsers) annofabcli.annotation_specs.subcommand_annotation_specs.add_parser(subparsers) @@ -59,7 +59,7 @@ def main(arguments: Optional[Sequence[str]] = None): else: args = parser.parse_args(arguments) - if hasattr(args, 'subcommand_func'): + if hasattr(args, "subcommand_func"): try: args.subcommand_func(args) except Exception as e: diff --git a/annofabcli/__version__.py b/annofabcli/__version__.py index 79dcb491..8c590a56 100644 --- a/annofabcli/__version__.py +++ b/annofabcli/__version__.py @@ -1 +1 @@ -__version__ = '1.15.9' +__version__ = "1.15.9" diff --git a/annofabcli/annotation/list_annotation_count.py b/annofabcli/annotation/list_annotation_count.py index 00fd4be1..abbc9925 100644 --- a/annofabcli/annotation/list_annotation_count.py +++ b/annofabcli/annotation/list_annotation_count.py @@ -20,54 +20,62 @@ class GroupBy(Enum): - TASK_ID = 'task_id' - INPUT_DATA_ID = 'input_data_id' + TASK_ID = "task_id" + INPUT_DATA_ID = "input_data_id" class ListAnnotationCount(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) @staticmethod - def _modify_attribute_of_query(attribute_query: Dict[str, Any], - additional_data_definition: AdditionalDataDefinition) -> Dict[str, Any]: - if 'choice_name_en' in attribute_query: + def _modify_attribute_of_query( + attribute_query: Dict[str, Any], additional_data_definition: AdditionalDataDefinition + ) -> Dict[str, Any]: + if "choice_name_en" in attribute_query: choice_info = more_itertools.first_true( - additional_data_definition['choices'], - pred=lambda e: AnnofabApiFacade.get_choice_name_en(e) == attribute_query['choice_name_en']) + additional_data_definition["choices"], + pred=lambda e: AnnofabApiFacade.get_choice_name_en(e) == attribute_query["choice_name_en"], + ) if choice_info is not None: - attribute_query['choice'] = choice_info['choice_id'] + attribute_query["choice"] = choice_info["choice_id"] else: logger.warning(f"choice_name_en = {attribute_query['choice_name_en']} の選択肢は存在しませんでした。") return attribute_query @staticmethod - def _find_additional_data_with_name(additional_data_definitions: List[AdditionalDataDefinition], - name: str) -> Optional[AdditionalDataDefinition]: + def _find_additional_data_with_name( + additional_data_definitions: List[AdditionalDataDefinition], name: str + ) -> Optional[AdditionalDataDefinition]: additional_data_definition = more_itertools.first_true( additional_data_definitions, - pred=lambda e: AnnofabApiFacade.get_additional_data_definition_name_en(e) == name) + pred=lambda e: AnnofabApiFacade.get_additional_data_definition_name_en(e) == name, + ) return additional_data_definition @staticmethod - def _find_additional_data_with_id(additional_data_definitions: List[AdditionalDataDefinition], - definition_id: str) -> Optional[AdditionalDataDefinition]: + def _find_additional_data_with_id( + additional_data_definitions: List[AdditionalDataDefinition], definition_id: str + ) -> Optional[AdditionalDataDefinition]: additional_data_definition = more_itertools.first_true( - additional_data_definitions, pred=lambda e: e['additional_data_definition_id'] == definition_id) + additional_data_definitions, pred=lambda e: e["additional_data_definition_id"] == definition_id + ) return additional_data_definition - def _modify_attributes_of_query(self, attributes_of_query: List[Dict[str, Any]], - definitions: List[AdditionalDataDefinition]) -> List[Dict[str, Any]]: + def _modify_attributes_of_query( + self, attributes_of_query: List[Dict[str, Any]], definitions: List[AdditionalDataDefinition] + ) -> List[Dict[str, Any]]: for attribute_query in attributes_of_query: - definition_name = attribute_query.get('additional_data_definition_name_en') + definition_name = attribute_query.get("additional_data_definition_name_en") if definition_name is not None: additional_data_definition = self._find_additional_data_with_name(definitions, definition_name) if additional_data_definition is None: @@ -77,10 +85,11 @@ def _modify_attributes_of_query(self, attributes_of_query: List[Dict[str, Any]], continue if additional_data_definition is not None: - attribute_query['additional_data_definition_id'] = additional_data_definition[ - 'additional_data_definition_id'] + attribute_query["additional_data_definition_id"] = additional_data_definition[ + "additional_data_definition_id" + ] - definition_id = attribute_query['additional_data_definition_id'] + definition_id = attribute_query["additional_data_definition_id"] additional_data_definition = self._find_additional_data_with_id(definitions, definition_id) if additional_data_definition is None: logger.warning( @@ -92,8 +101,9 @@ def _modify_attributes_of_query(self, attributes_of_query: List[Dict[str, Any]], return attributes_of_query - def _modify_annotation_query(self, project_id: str, annotation_query: Dict[str, Any], - task_id: Optional[str] = None) -> Dict[str, Any]: + def _modify_annotation_query( + self, project_id: str, annotation_query: Dict[str, Any], task_id: Optional[str] = None + ) -> Dict[str, Any]: """ タスク検索クエリを修正する。 * ``label_name_en`` から ``label_id`` に変換する。 @@ -111,56 +121,60 @@ def _modify_annotation_query(self, project_id: str, annotation_query: Dict[str, """ annotation_specs, _ = self.service.api.get_annotation_specs(project_id) - specs_labels = annotation_specs['labels'] + specs_labels = annotation_specs["labels"] # label_name_en から label_idを設定 - if 'label_name_en' in annotation_query: - label_name_en = annotation_query['label_name_en'] - label = more_itertools.first_true(specs_labels, - pred=lambda e: AnnofabApiFacade.get_label_name_en(e) == label_name_en) + if "label_name_en" in annotation_query: + label_name_en = annotation_query["label_name_en"] + label = more_itertools.first_true( + specs_labels, pred=lambda e: AnnofabApiFacade.get_label_name_en(e) == label_name_en + ) if label is not None: - annotation_query['label_id'] = label['label_id'] + annotation_query["label_id"] = label["label_id"] else: logger.warning(f"label_name_en: {label_name_en} の label_id が見つかりませんでした。") - if annotation_query.keys() >= {'label_id', 'attributes'}: - label = more_itertools.first_true(specs_labels, - pred=lambda e: e['label_id'] == annotation_query['label_id']) + if annotation_query.keys() >= {"label_id", "attributes"}: + label = more_itertools.first_true( + specs_labels, pred=lambda e: e["label_id"] == annotation_query["label_id"] + ) if label is not None: - self._modify_attributes_of_query(annotation_query['attributes'], label['additional_data_definitions']) + self._modify_attributes_of_query(annotation_query["attributes"], label["additional_data_definitions"]) else: logger.warning(f"label_id: {annotation_query['label_id']} の label_id が見つかりませんでした。") if task_id is not None: - annotation_query['task_id'] = task_id - annotation_query['exact_match_task_id'] = True + annotation_query["task_id"] = task_id + annotation_query["exact_match_task_id"] = True return annotation_query @staticmethod def aggregate_annotations(annotations: List[SingleAnnotation], group_by: GroupBy) -> pandas.DataFrame: df = pandas.DataFrame(annotations) - df = df[['task_id', 'input_data_id']] - df['annotation_count'] = 1 + df = df[["task_id", "input_data_id"]] + df["annotation_count"] = 1 if group_by == GroupBy.INPUT_DATA_ID: - return df.groupby(['task_id', 'input_data_id'], as_index=False).count() + return df.groupby(["task_id", "input_data_id"], as_index=False).count() elif group_by == GroupBy.TASK_ID: - return df.groupby(['task_id'], as_index=False).count().drop(['input_data_id'], axis=1) + return df.groupby(["task_id"], as_index=False).count().drop(["input_data_id"], axis=1) else: return pandas.DataFrame() - def get_annotations(self, project_id: str, annotation_query: Dict[str, Any], - task_id: Optional[str] = None) -> List[SingleAnnotation]: + def get_annotations( + self, project_id: str, annotation_query: Dict[str, Any], task_id: Optional[str] = None + ) -> List[SingleAnnotation]: annotation_query = self._modify_annotation_query(project_id, annotation_query, task_id) logger.debug(f"annotation_query: {annotation_query}") - annotations = self.service.wrapper.get_all_annotation_list(project_id, query_params={'query': annotation_query}) + annotations = self.service.wrapper.get_all_annotation_list(project_id, query_params={"query": annotation_query}) return annotations - def list_annotations(self, project_id: str, annotation_query: Dict[str, Any], group_by: GroupBy, - task_id_list: List[str]): + def list_annotations( + self, project_id: str, annotation_query: Dict[str, Any], group_by: GroupBy, task_id_list: List[str] + ): """ アノテーション一覧を出力する """ @@ -191,8 +205,9 @@ def main(self): group_by = GroupBy(args.group_by) task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id) - self.list_annotations(args.project_id, annotation_query=annotation_query, group_by=group_by, - task_id_list=task_id_list) + self.list_annotations( + args.project_id, annotation_query=annotation_query, group_by=group_by, task_id_list=task_id_list + ) def main(args): @@ -207,23 +222,32 @@ def parse_args(parser: argparse.ArgumentParser): argument_parser.add_project_id() parser.add_argument( - '-aq', - '--annotation_query', + "-aq", + "--annotation_query", type=str, required=True, - help='アノテーションの検索クエリをJSON形式で指定します。' - '`file://`を先頭に付けると、JSON形式のファイルを指定できます。' - 'クエリのフォーマットは、[getAnnotationList API](https://annofab.com/docs/api/#operation/getAnnotationList)のクエリパラメータの`query`キー配下と同じです。' # noqa: E501 - 'さらに追加で、`label_name_en`(label_idに対応), `additional_data_definition_name_en`(additional_data_definition_idに対応) キーも指定できます。' # noqa: E501 + help="アノテーションの検索クエリをJSON形式で指定します。" + "`file://`を先頭に付けると、JSON形式のファイルを指定できます。" + "クエリのフォーマットは、[getAnnotationList API](https://annofab.com/docs/api/#operation/getAnnotationList)のクエリパラメータの`query`キー配下と同じです。" # noqa: E501 + "さらに追加で、`label_name_en`(label_idに対応), `additional_data_definition_name_en`(additional_data_definition_idに対応) キーも指定できます。", # noqa: E501 ) argument_parser.add_task_id( - required=False, help_message=('対象のタスクのtask_idを指定します。' - '`file://`を先頭に付けると、task_idの一覧が記載されたファイルを指定できます。' - '指定した場合、`--annotation_query`のtask_id, exact_match_task_idが上書きされます')) + required=False, + help_message=( + "対象のタスクのtask_idを指定します。" + "`file://`を先頭に付けると、task_idの一覧が記載されたファイルを指定できます。" + "指定した場合、`--annotation_query`のtask_id, exact_match_task_idが上書きされます" + ), + ) - parser.add_argument('--group_by', type=str, choices=[GroupBy.TASK_ID.value, GroupBy.INPUT_DATA_ID.value], - default=GroupBy.TASK_ID.value, help='アノテーションの個数をどの単位で集約するかを指定してます。') + parser.add_argument( + "--group_by", + type=str, + choices=[GroupBy.TASK_ID.value, GroupBy.INPUT_DATA_ID.value], + default=GroupBy.TASK_ID.value, + help="アノテーションの個数をどの単位で集約するかを指定してます。", + ) argument_parser.add_output() argument_parser.add_csv_format() @@ -234,7 +258,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "list_count" subcommand_help = "task_idまたはinput_data_idで集約したアノテーションの個数を出力します。" - description = ("task_idまたはinput_data_idで集約したアノテーションの個数を出力します。") + description = "task_idまたはinput_data_idで集約したアノテーションの個数を出力します。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description) parse_args(parser) diff --git a/annofabcli/annotation/subcommand_annotation.py b/annofabcli/annotation/subcommand_annotation.py index dc67ca74..65aa763a 100644 --- a/annofabcli/annotation/subcommand_annotation.py +++ b/annofabcli/annotation/subcommand_annotation.py @@ -7,7 +7,7 @@ def parse_args(parser: argparse.ArgumentParser): - subparsers = parser.add_subparsers(dest='subcommand_name') + subparsers = parser.add_subparsers(dest="subcommand_name") # サブコマンドの定義 annofabcli.annotation.list_annotation_count.add_parser(subparsers) diff --git a/annofabcli/annotation_specs/list_annotation_specs_history.py b/annofabcli/annotation_specs/list_annotation_specs_history.py index aaf9a5fa..da8ccc70 100644 --- a/annofabcli/annotation_specs/list_annotation_specs_history.py +++ b/annofabcli/annotation_specs/list_annotation_specs_history.py @@ -17,6 +17,7 @@ class AnnotationSpecsHistories(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) @@ -47,8 +48,9 @@ def parse_args(parser: argparse.ArgumentParser): argument_parser.add_project_id() - argument_parser.add_format(choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON], - default=FormatArgument.CSV) + argument_parser.add_format( + choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON], default=FormatArgument.CSV + ) argument_parser.add_output() argument_parser.add_csv_format() argument_parser.add_query() @@ -59,7 +61,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "history" subcommand_help = "アノテーション仕様の変更履歴を表示する。" - description = ("アノテーション仕様の変更履歴を表示する。") + description = "アノテーション仕様の変更履歴を表示する。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description) parse_args(parser) diff --git a/annofabcli/annotation_specs/list_annotation_specs_label.py b/annofabcli/annotation_specs/list_annotation_specs_label.py index 94cf7420..74492cef 100644 --- a/annofabcli/annotation_specs/list_annotation_specs_label.py +++ b/annofabcli/annotation_specs/list_annotation_specs_label.py @@ -25,19 +25,21 @@ class PrintAnnotationSpecsLabel(AbstractCommandLineInterface): COMMON_MESSAGE = "annofabcli annotation_specs list_label: error:" - def print_annotation_specs_label(self, project_id: str, arg_format: str, output: Optional[str] = None, - history_id: Optional[str] = None): + def print_annotation_specs_label( + self, project_id: str, arg_format: str, output: Optional[str] = None, history_id: Optional[str] = None + ): super().validate_project(project_id, [ProjectMemberRole.OWNER, ProjectMemberRole.ACCEPTER]) annotation_specs = self.service.api.get_annotation_specs(project_id, query_params={"history_id": history_id})[0] - labels = annotation_specs['labels'] + labels = annotation_specs["labels"] - if arg_format == 'text': + if arg_format == "text": self._print_text_format_labels(labels, output=output) elif arg_format in [FormatArgument.JSON.value, FormatArgument.PRETTY_JSON.value]: - annofabcli.utils.print_according_to_format(target=labels, arg_format=FormatArgument(arg_format), - output=output) + annofabcli.utils.print_according_to_format( + target=labels, arg_format=FormatArgument(arg_format), output=output + ) @staticmethod def _get_name_list(messages: List[Dict[str, Any]]): @@ -52,24 +54,31 @@ def _get_name_list(messages: List[Dict[str, Any]]): def _print_text_format_labels(labels, output: Optional[str] = None): output_lines = [] for label in labels: - output_lines.append('\t'.join([ - label['label_id'], - label['annotation_type'], - ] + PrintAnnotationSpecsLabel._get_name_list(label['label_name']['messages']))) - for additional_data_definition in label['additional_data_definitions']: - output_lines.append('\t'.join([ - '', - additional_data_definition['additional_data_definition_id'], - additional_data_definition['type'], - ] + PrintAnnotationSpecsLabel._get_name_list(additional_data_definition['name']['messages']))) - if additional_data_definition['type'] in ['choice', 'select']: - for choice in additional_data_definition['choices']: - output_lines.append('\t'.join([ - '', - '', - choice['choice_id'], - '', - ] + PrintAnnotationSpecsLabel._get_name_list(choice['name']['messages']))) + output_lines.append( + "\t".join( + [label["label_id"], label["annotation_type"],] + + PrintAnnotationSpecsLabel._get_name_list(label["label_name"]["messages"]) + ) + ) + for additional_data_definition in label["additional_data_definitions"]: + output_lines.append( + "\t".join( + [ + "", + additional_data_definition["additional_data_definition_id"], + additional_data_definition["type"], + ] + + PrintAnnotationSpecsLabel._get_name_list(additional_data_definition["name"]["messages"]) + ) + ) + if additional_data_definition["type"] in ["choice", "select"]: + for choice in additional_data_definition["choices"]: + output_lines.append( + "\t".join( + ["", "", choice["choice_id"], "",] + + PrintAnnotationSpecsLabel._get_name_list(choice["name"]["messages"]) + ) + ) annofabcli.utils.output_string("\n".join(output_lines), output) @@ -79,9 +88,11 @@ def get_history_id_from_before_index(self, project_id: str, before: int): logger.warning(f"アノテーション仕様の履歴は{len(histories)}個のため、最新より{before}個前のアノテーション仕様は見つかりませんでした。") return None history = histories[-(before + 1)] - logger.info(f"{history['updated_datetime']}のアノテーション仕様を出力します。" - f"history_id={history['history_id']}, comment={history['comment']}") - return histories[-before + 1]['history_id'] + logger.info( + f"{history['updated_datetime']}のアノテーション仕様を出力します。" + f"history_id={history['history_id']}, comment={history['comment']}" + ) + return histories[-before + 1]["history_id"] def main(self): @@ -90,15 +101,18 @@ def main(self): if args.before is not None: history_id = self.get_history_id_from_before_index(args.project_id, args.before) if history_id is None: - print(f"{self.COMMON_MESSAGE} argument --before: 最新より{args.before}個前のアノテーション仕様は見つかりませんでした。", - file=sys.stderr) + print( + f"{self.COMMON_MESSAGE} argument --before: 最新より{args.before}個前のアノテーション仕様は見つかりませんでした。", + file=sys.stderr, + ) return else: # args.beforeがNoneならば、必ずargs.history_idはNoneでない history_id = args.history_id - self.print_annotation_specs_label(args.project_id, arg_format=args.format, output=args.output, - history_id=history_id) + self.print_annotation_specs_label( + args.project_id, arg_format=args.format, output=args.output, history_id=history_id + ) def parse_args(parser: argparse.ArgumentParser): @@ -109,21 +123,36 @@ def parse_args(parser: argparse.ArgumentParser): # 過去のアノテーション仕様を参照するためのオプション old_annotation_specs_group = parser.add_mutually_exclusive_group() old_annotation_specs_group.add_argument( - '--history_id', type=str, help=("出力したい過去のアノテーション仕様のhistory_idを指定してください。 " - "history_idは`annotation_specs history`コマンドで確認できます。 " - "指定しない場合は、最新のアノテーション仕様が出力されます。 ")) + "--history_id", + type=str, + help=( + "出力したい過去のアノテーション仕様のhistory_idを指定してください。 " + "history_idは`annotation_specs history`コマンドで確認できます。 " + "指定しない場合は、最新のアノテーション仕様が出力されます。 " + ), + ) old_annotation_specs_group.add_argument( - '--before', type=int, help=("出力したい過去のアノテーション仕様が、最新よりいくつ前のアノテーション仕様であるかを指定してください。 " - "たとえば`1`を指定した場合、最新より1個前のアノテーション仕様を出力します。 " - "指定しない場合は、最新のアノテーション仕様が出力されます。 ")) + "--before", + type=int, + help=( + "出力したい過去のアノテーション仕様が、最新よりいくつ前のアノテーション仕様であるかを指定してください。 " + "たとえば`1`を指定した場合、最新より1個前のアノテーション仕様を出力します。 " + "指定しない場合は、最新のアノテーション仕様が出力されます。 " + ), + ) parser.add_argument( - '-f', '--format', type=str, choices=['text', FormatArgument.PRETTY_JSON.value, FormatArgument.JSON.value], - default='text', help=f'出力フォーマット ' - 'text: 人が見やすい形式, ' - f'{FormatArgument.PRETTY_JSON.value}: インデントされたJSON, ' - f'{FormatArgument.JSON.value}: フラットなJSON') + "-f", + "--format", + type=str, + choices=["text", FormatArgument.PRETTY_JSON.value, FormatArgument.JSON.value], + default="text", + help=f"出力フォーマット " + "text: 人が見やすい形式, " + f"{FormatArgument.PRETTY_JSON.value}: インデントされたJSON, " + f"{FormatArgument.JSON.value}: フラットなJSON", + ) argument_parser.add_output() @@ -137,13 +166,13 @@ def main(args): def add_parser(subparsers: argparse._SubParsersAction): - subcommand_name = 'list_label' + subcommand_name = "list_label" - subcommand_help = ('アノテーション仕様のラベル情報を出力する') + subcommand_help = "アノテーション仕様のラベル情報を出力する" - description = ('アノテーション仕様のラベル情報を出力する') + description = "アノテーション仕様のラベル情報を出力する" - epilog = 'チェッカーまたはオーナロールを持つユーザで実行してください。' + epilog = "チェッカーまたはオーナロールを持つユーザで実行してください。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog) parse_args(parser) diff --git a/annofabcli/annotation_specs/print_label_color.py b/annofabcli/annotation_specs/print_label_color.py index a5cd51b4..ff4e96d3 100644 --- a/annofabcli/annotation_specs/print_label_color.py +++ b/annofabcli/annotation_specs/print_label_color.py @@ -11,8 +11,12 @@ import annofabcli import annofabcli.common.cli from annofabcli import AnnofabApiFacade -from annofabcli.common.cli import (AbstractCommandLineInterface, ArgumentParser, FormatArgument, - build_annofabapi_resource_and_login) +from annofabcli.common.cli import ( + AbstractCommandLineInterface, + ArgumentParser, + FormatArgument, + build_annofabapi_resource_and_login, +) logger = logging.getLogger(__name__) @@ -21,6 +25,7 @@ class PrintLabelColor(AbstractCommandLineInterface): """ アノテーションラベルの色(RGB)を出力する """ + @staticmethod def get_rgb(label: Dict[str, Any]) -> Tuple[int, int, int]: color = label["color"] @@ -51,8 +56,9 @@ def parse_args(parser: argparse.ArgumentParser): argument_parser.add_project_id() - argument_parser.add_format(choices=[FormatArgument.JSON, FormatArgument.PRETTY_JSON], - default=FormatArgument.PRETTY_JSON) + argument_parser.add_format( + choices=[FormatArgument.JSON, FormatArgument.PRETTY_JSON], default=FormatArgument.PRETTY_JSON + ) argument_parser.add_output() @@ -68,11 +74,13 @@ def main(args): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "list_label_color" - subcommand_help = ("label_name(英名)とRGBの関係をJSONで出力します。") + subcommand_help = "label_name(英名)とRGBの関係をJSONで出力します。" - description = ("label_name(英名)とRGBの関係をJSONで出力します。" - "出力された内容は、`write_annotation_image`ツールに利用します。" - "出力内容は`Dict[LabelName, [R,G,B]]`です。") + description = ( + "label_name(英名)とRGBの関係をJSONで出力します。" + "出力された内容は、`write_annotation_image`ツールに利用します。" + "出力内容は`Dict[LabelName, [R,G,B]]`です。" + ) epilog = "チェッカーまたはオーナロールを持つユーザで実行してください。" diff --git a/annofabcli/annotation_specs/subcommand_annotation_specs.py b/annofabcli/annotation_specs/subcommand_annotation_specs.py index 862c017b..f81a4133 100644 --- a/annofabcli/annotation_specs/subcommand_annotation_specs.py +++ b/annofabcli/annotation_specs/subcommand_annotation_specs.py @@ -9,7 +9,7 @@ def parse_args(parser: argparse.ArgumentParser): - subparsers = parser.add_subparsers(dest='subcommand_name') + subparsers = parser.add_subparsers(dest="subcommand_name") # サブコマンドの定義 annofabcli.annotation_specs.list_annotation_specs_history.add_parser(subparsers) diff --git a/annofabcli/common/cli.py b/annofabcli/common/cli.py index 6dd8ce0c..60fad080 100644 --- a/annofabcli/common/cli.py +++ b/annofabcli/common/cli.py @@ -46,8 +46,13 @@ def build_annofabapi_resource_and_login() -> annofabapi.Resource: raise e -def add_parser(subparsers: argparse._SubParsersAction, subcommand_name: str, subcommand_help: str, description: str, - epilog: Optional[str] = None) -> argparse.ArgumentParser: +def add_parser( + subparsers: argparse._SubParsersAction, + subcommand_name: str, + subcommand_help: str, + description: str, + epilog: Optional[str] = None, +) -> argparse.ArgumentParser: """ サブコマンド用にparserを追加する @@ -62,8 +67,9 @@ def add_parser(subparsers: argparse._SubParsersAction, subcommand_name: str, sub サブコマンドのparser """ - parser = subparsers.add_parser(subcommand_name, parents=[create_parent_parser()], description=description, - help=subcommand_help, epilog=epilog) + parser = subparsers.add_parser( + subcommand_name, parents=[create_parent_parser()], description=description, help=subcommand_help, epilog=epilog + ) parser.set_defaults(command_help=parser.print_help) return parser @@ -75,17 +81,21 @@ def create_parent_parser() -> argparse.ArgumentParser: parent_parser = argparse.ArgumentParser(add_help=False) group = parent_parser.add_argument_group("global optional arguments") - group.add_argument('--yes', action="store_true", help="処理中に現れる問い合わせに対して、常に'yes'と回答します。") + group.add_argument("--yes", action="store_true", help="処理中に現れる問い合わせに対して、常に'yes'と回答します。") - group.add_argument('--logdir', type=str, default=".log", - help="ログファイルを保存するディレクトリを指定します。指定しない場合は`.log`ディレクトリ'にログファイルが保存されます。") + group.add_argument( + "--logdir", type=str, default=".log", help="ログファイルを保存するディレクトリを指定します。指定しない場合は`.log`ディレクトリ'にログファイルが保存されます。" + ) - group.add_argument('--disable_log', action="store_true", help="ログを無効にします。") + group.add_argument("--disable_log", action="store_true", help="ログを無効にします。") group.add_argument( - "--logging_yaml", type=str, help="ロギグングの設定ファイル(YAML)を指定します。指定した場合、`--logdir`オプションは無視されます。" + "--logging_yaml", + type=str, + help="ロギグングの設定ファイル(YAML)を指定します。指定した場合、`--logdir`オプションは無視されます。" "指定しない場合、デフォルトのロギングが設定されます。" - "設定ファイルの書き方は https://docs.python.org/ja/3/howto/logging.html 参照してください。") + "設定ファイルの書き方は https://docs.python.org/ja/3/howto/logging.html 参照してください。", + ) return parent_parser @@ -209,10 +219,10 @@ def prompt_yesno(msg: str) -> bool: """ while True: choice = input(f"{msg} [y/N] : ") - if choice == 'y': + if choice == "y": return True - elif choice == 'N': + elif choice == "N": return False @@ -228,13 +238,13 @@ def prompt_yesnoall(msg: str) -> Tuple[bool, bool]: """ while True: choice = input(f"{msg} [y/N/ALL] : ") - if choice == 'y': + if choice == "y": return True, False - elif choice == 'N': + elif choice == "N": return False, False - elif choice == 'ALL': + elif choice == "ALL": return True, True @@ -242,6 +252,7 @@ class ArgumentParser: """ 共通のコマンドライン引数を追加するためのクラス """ + def __init__(self, parser: argparse.ArgumentParser): self.parser = parser @@ -250,37 +261,38 @@ def add_project_id(self, help_message: Optional[str] = None): '--project_id` 引数を追加 """ if help_message is None: - help_message = '対象のプロジェクトのproject_idを指定します。' + help_message = "対象のプロジェクトのproject_idを指定します。" - self.parser.add_argument('-p', '--project_id', type=str, required=True, help=help_message) + self.parser.add_argument("-p", "--project_id", type=str, required=True, help=help_message) def add_task_id(self, required: bool = True, help_message: Optional[str] = None): """ '--task_id` 引数を追加 """ if help_message is None: - help_message = ('対象のタスクのtask_idを指定します。' '`file://`を先頭に付けると、task_idの一覧が記載されたファイルを指定できます。') + help_message = "対象のタスクのtask_idを指定します。" "`file://`を先頭に付けると、task_idの一覧が記載されたファイルを指定できます。" - self.parser.add_argument('-t', '--task_id', type=str, required=required, nargs='+', help=help_message) + self.parser.add_argument("-t", "--task_id", type=str, required=required, nargs="+", help=help_message) def add_input_data_id(self, required: bool = True, help_message: Optional[str] = None): """ '--input_data_id` 引数を追加 """ if help_message is None: - help_message = ('対象の入力データのinput_data_idを指定します。' '`file://`を先頭に付けると、input_data_idの一覧が記載されたファイルを指定できます。') + help_message = "対象の入力データのinput_data_idを指定します。" "`file://`を先頭に付けると、input_data_idの一覧が記載されたファイルを指定できます。" - self.parser.add_argument('-i', '--input_data_id', type=str, required=required, nargs='+', help=help_message) + self.parser.add_argument("-i", "--input_data_id", type=str, required=required, nargs="+", help=help_message) def add_format(self, choices: List[FormatArgument], default: FormatArgument, help_message: Optional[str] = None): """ '--format` 引数を追加 """ if help_message is None: - help_message = (f'出力フォーマットを指定します。指定しない場合は、{default.value} フォーマットになります。') + help_message = f"出力フォーマットを指定します。指定しない場合は、{default.value} フォーマットになります。" - self.parser.add_argument('-f', '--format', type=str, choices=[e.value for e in choices], default=default.value, - help=help_message) + self.parser.add_argument( + "-f", "--format", type=str, choices=[e.value for e in choices], default=default.value, help=help_message + ) def add_csv_format(self, help_message: Optional[str] = None): """ @@ -288,30 +300,30 @@ def add_csv_format(self, help_message: Optional[str] = None): """ if help_message is None: help_message = ( - 'CSVのフォーマットをJSON形式で指定します。`--format`が`csv`でないときは、このオプションは無視されます。' - '`file://`を先頭に付けると、JSON形式のファイルを指定できます。' - '指定した値は、[pandas.DataFrame.to_csv](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.to_csv.html) の引数として渡されます。' # noqa: E501 + "CSVのフォーマットをJSON形式で指定します。`--format`が`csv`でないときは、このオプションは無視されます。" + "`file://`を先頭に付けると、JSON形式のファイルを指定できます。" + "指定した値は、[pandas.DataFrame.to_csv](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.to_csv.html) の引数として渡されます。" # noqa: E501 ) - self.parser.add_argument('--csv_format', type=str, help=help_message) + self.parser.add_argument("--csv_format", type=str, help=help_message) def add_output(self, required: bool = False, help_message: Optional[str] = None): """ '--output` 引数を追加 """ if help_message is None: - help_message = '出力先のファイルパスを指定します。指定しない場合は、標準出力に出力されます。' + help_message = "出力先のファイルパスを指定します。指定しない場合は、標準出力に出力されます。" - self.parser.add_argument('-o', '--output', type=str, required=required, help=help_message) + self.parser.add_argument("-o", "--output", type=str, required=required, help=help_message) def add_query(self, help_message: Optional[str] = None): """ '--query` 引数を追加 """ if help_message is None: - help_message = 'JMESPath形式で指定します。出力結果の抽出や、出力内容の変更に利用できます。' + help_message = "JMESPath形式で指定します。出力結果の抽出や、出力内容の変更に利用できます。" - self.parser.add_argument('-q', '--query', type=str, help=help_message) + self.parser.add_argument("-q", "--query", type=str, help=help_message) class AbstractCommandLineInterface(abc.ABC): @@ -356,22 +368,26 @@ def process_common_args(self, args: argparse.Namespace): load_logging_config_from_args(args) self.all_yes = args.yes - if hasattr(args, 'query'): + if hasattr(args, "query"): self.query = args.query - if hasattr(args, 'csv_format'): + if hasattr(args, "csv_format"): self.csv_format = annofabcli.common.cli.get_csv_format_from_args(args.csv_format) - if hasattr(args, 'output'): + if hasattr(args, "output"): self.output = args.output - if hasattr(args, 'format'): + if hasattr(args, "format"): self.str_format = args.format logger.info(f"args: {args}") - def validate_project(self, project_id, project_member_roles: Optional[List[ProjectMemberRole]] = None, - organization_member_roles: Optional[List[OrganizationMemberRole]] = None): + def validate_project( + self, + project_id, + project_member_roles: Optional[List[ProjectMemberRole]] = None, + organization_member_roles: Optional[List[OrganizationMemberRole]] = None, + ): """ プロジェクト or 組織に対して、必要な権限が付与されているかを確認する。 Args: @@ -465,5 +481,6 @@ def print_csv(self, df: pandas.DataFrame): def print_according_to_format(self, target: Any): target = self.search_with_jmespath_expression(target) - annofabcli.utils.print_according_to_format(target, arg_format=FormatArgument(self.str_format), - output=self.output, csv_format=self.csv_format) + annofabcli.utils.print_according_to_format( + target, arg_format=FormatArgument(self.str_format), output=self.output, csv_format=self.csv_format + ) diff --git a/annofabcli/common/dataclasses.py b/annofabcli/common/dataclasses.py index 6956cb56..f7dc2554 100644 --- a/annofabcli/common/dataclasses.py +++ b/annofabcli/common/dataclasses.py @@ -9,6 +9,7 @@ class WaitOptions: """ 最新化ジョブが完了するまで待つときのオプション """ + interval: int """ジョブにアクセスする間隔[秒]""" diff --git a/annofabcli/common/enums.py b/annofabcli/common/enums.py index 2592a837..4da0f7ea 100644 --- a/annofabcli/common/enums.py +++ b/annofabcli/common/enums.py @@ -13,25 +13,25 @@ class FormatArgument(Enum): """ #: CSV形式 - CSV = 'csv' + CSV = "csv" #: JSON形式 - JSON = 'json' + JSON = "json" #: インデントされたJSON形式 - PRETTY_JSON = 'pretty_json' + PRETTY_JSON = "pretty_json" #: input_data_idの一覧 - INPUT_DATA_ID_LIST = 'input_data_id_list' + INPUT_DATA_ID_LIST = "input_data_id_list" #: task_idの一覧 - TASK_ID_LIST = 'task_id_list' + TASK_ID_LIST = "task_id_list" #: inspection_idの一覧 - INSPECTION_ID_LIST = 'inspection_id_list' + INSPECTION_ID_LIST = "inspection_id_list" #: user_idの一覧 - USER_ID_LIST = 'user_id_list' + USER_ID_LIST = "user_id_list" #: project_idの一覧 - PROJECT_ID_LIST = 'project_id_list' + PROJECT_ID_LIST = "project_id_list" diff --git a/annofabcli/common/exceptions.py b/annofabcli/common/exceptions.py index 0afe7624..6ba17f73 100644 --- a/annofabcli/common/exceptions.py +++ b/annofabcli/common/exceptions.py @@ -19,6 +19,7 @@ class AuthenticationError(AnnofabCliException): """ AnnoFabの認証エラー """ + def __init__(self, loing_user_id: str): msg = f"AnnoFabにログインできませんでした。User ID: {loing_user_id}" super().__init__(msg) @@ -32,6 +33,7 @@ class ProjectAuthorizationError(AuthorizationError): """ AnnoFabプロジェクトに関する認可エラー """ + def __init__(self, project_title: str, roles: List[ProjectMemberRole]): role_values = [e.value for e in roles] msg = f"プロジェクト: {project_title} に、ロール: {role_values} のいずれかが付与されていません。" @@ -42,6 +44,7 @@ class OrganizationAuthorizationError(AuthorizationError): """ AnnoFab組織に関する認可エラー """ + def __init__(self, organization_name: str, roles: List[OrganizationMemberRole]): role_values = [e.value for e in roles] msg = f"組織: {organization_name} に、ロール: {role_values} のいずれかが付与されていません。" diff --git a/annofabcli/common/facade.py b/annofabcli/common/facade.py index 1cc1fa46..0eab0dd2 100644 --- a/annofabcli/common/facade.py +++ b/annofabcli/common/facade.py @@ -68,7 +68,7 @@ def get_project_title(self, project_id: str) -> str: """ project, _ = self.service.api.get_project(project_id) - return project['title'] + return project["title"] def get_my_account_id(self) -> str: """ @@ -78,10 +78,11 @@ def get_my_account_id(self) -> str: """ account, _ = self.service.api.get_my_account() - return account['account_id'] + return account["account_id"] - def _get_organization_member_with_predicate(self, project_id: str, - predicate: Callable[[Any], bool]) -> Optional[OrganizationMember]: + def _get_organization_member_with_predicate( + self, project_id: str, predicate: Callable[[Any], bool] + ) -> Optional[OrganizationMember]: """ account_idから組織メンバを取得する。 インスタンス変数に組織メンバがあれば、WebAPIは実行しない。 @@ -93,6 +94,7 @@ def _get_organization_member_with_predicate(self, project_id: str, Returns: 組織メンバ。見つからない場合はNone """ + def update_organization_members(): organization_name = self.get_organization_name_from_project_id(project_id) members = self.service.wrapper.get_all_organization_members(organization_name) @@ -157,7 +159,7 @@ def get_user_id_from_account_id(self, project_id: str, account_id: str) -> Optio if member is None: return None else: - return member.get('user_id') + return member.get("user_id") def get_account_id_from_user_id(self, project_id: str, user_id: str) -> Optional[str]: """ @@ -176,7 +178,7 @@ def get_account_id_from_user_id(self, project_id: str, user_id: str) -> Optional if member is None: return None else: - return member.get('account_id') + return member.get("account_id") def get_organization_name_from_project_id(self, project_id: str) -> str: """ @@ -208,8 +210,9 @@ def contains_any_project_member_role(self, project_id: str, roles: List[ProjectM my_role = ProjectMemberRole(my_member["member_role"]) return my_role in roles - def contains_any_organization_member_role(self, organization_name: str, - roles: List[OrganizationMemberRole]) -> bool: + def contains_any_organization_member_role( + self, organization_name: str, roles: List[OrganizationMemberRole] + ) -> bool: """ 自分自身の組織メンバとしてのロールが、指定されたロールのいずれかに合致するかどうか Args: @@ -234,8 +237,9 @@ def contains_any_organization_member_role(self, organization_name: str, # operateTaskのfacade ################## - def change_operator_of_task(self, project_id: str, task_id: str, - account_id: Optional[str] = None) -> Dict[str, Any]: + def change_operator_of_task( + self, project_id: str, task_id: str, account_id: Optional[str] = None + ) -> Dict[str, Any]: """ タスクの担当者を変更する Args: @@ -294,8 +298,9 @@ def change_to_break_phase(self, project_id: str, task_id: str, account_id: str) } return self.service.api.operate_task(project_id, task_id, request_body=req)[0] - def reject_task(self, project_id: str, task_id: str, account_id: str, - annotator_account_id: Optional[str] = None) -> Dict[str, Any]: + def reject_task( + self, project_id: str, task_id: str, account_id: str, annotator_account_id: Optional[str] = None + ) -> Dict[str, Any]: """ タスクを差し戻し、annotator_account_id に担当を割り当てる。 Args: @@ -326,8 +331,9 @@ def reject_task(self, project_id: str, task_id: str, account_id: str, updated_task, _ = self.service.api.operate_task(project_id, task["task_id"], request_body=req_change_operator) return updated_task - def reject_task_assign_last_annotator(self, project_id: str, task_id: str, - account_id: str) -> Tuple[Dict[str, Any], str]: + def reject_task_assign_last_annotator( + self, project_id: str, task_id: str, account_id: str + ) -> Tuple[Dict[str, Any], str]: """ タスクを差し戻したあとに、最後のannotation phase担当者に割り当てる。 diff --git a/annofabcli/common/image.py b/annofabcli/common/image.py index 04fddb91..d5d8cb1b 100644 --- a/annofabcli/common/image.py +++ b/annofabcli/common/image.py @@ -39,8 +39,12 @@ def get_data_uri_of_outer_file(annotation: SimpleAnnotationDetail) -> Optional[s return data.get("data_uri") -def fill_annotation(draw: PIL.ImageDraw.Draw, annotation: SimpleAnnotationDetail, label_color_dict: Dict[str, RGB], - outer_image: Optional[Any] = None) -> PIL.ImageDraw.Draw: +def fill_annotation( + draw: PIL.ImageDraw.Draw, + annotation: SimpleAnnotationDetail, + label_color_dict: Dict[str, RGB], + outer_image: Optional[Any] = None, +) -> PIL.ImageDraw.Draw: """ 1個のアノテーションを、塗りつぶしで描画する。(矩形、ポリゴン、塗りつぶし、塗りつぶしv2) @@ -79,14 +83,16 @@ def fill_annotation(draw: PIL.ImageDraw.Draw, annotation: SimpleAnnotationDetail if outer_image is not None: draw.bitmap([0, 0], outer_image, fill=color) else: - logger.warning(f"アノテーション種類が`{data_type}`ですが、`outer_image`がNoneです。 " - f"annotation_id={annotation.annotation_id}") + logger.warning( + f"アノテーション種類が`{data_type}`ですが、`outer_image`がNoneです。 " f"annotation_id={annotation.annotation_id}" + ) return draw -def fill_annotation_list(draw: PIL.ImageDraw.Draw, parser: SimpleAnnotationParser, - label_color_dict: Dict[str, RGB]) -> PIL.ImageDraw.Draw: +def fill_annotation_list( + draw: PIL.ImageDraw.Draw, parser: SimpleAnnotationParser, label_color_dict: Dict[str, RGB] +) -> PIL.ImageDraw.Draw: """ 1個の入力データに属するアノテーションlistを描画する @@ -108,8 +114,9 @@ def fill_annotation_list(draw: PIL.ImageDraw.Draw, parser: SimpleAnnotationParse with parser.open_outer_file(data_uri_outer_image) as f: outer_image = PIL.Image.open(f) # アノテーション情報を描画する - fill_annotation(draw=draw, annotation=annotation, label_color_dict=label_color_dict, - outer_image=outer_image) + fill_annotation( + draw=draw, annotation=annotation, label_color_dict=label_color_dict, outer_image=outer_image + ) except AnnotationOuterFileNotFoundError as e: logger.warning(str(e)) @@ -121,8 +128,13 @@ def fill_annotation_list(draw: PIL.ImageDraw.Draw, parser: SimpleAnnotationParse return draw -def write_annotation_image(parser: SimpleAnnotationParser, image_size: InputDataSize, label_color_dict: Dict[str, RGB], - output_image_file: Path, background_color: Optional[Any] = None): +def write_annotation_image( + parser: SimpleAnnotationParser, + image_size: InputDataSize, + label_color_dict: Dict[str, RGB], + output_image_file: Path, + background_color: Optional[Any] = None, +): """ JSONファイルに記載されているアノテーション情報を、画像化する。 JSONファイルは、AnnoFabからダウンロードしたアノテーションzipに含まれるファイルを想定している。 @@ -160,10 +172,15 @@ def write_annotation_image(parser: SimpleAnnotationParser, image_size: InputData image.save(output_image_file) -def write_annotation_images_from_path(annotation_path: Path, image_size: InputDataSize, - label_color_dict: Dict[str, RGB], output_dir_path: Path, - output_image_extension: str = "png", background_color: Optional[Any] = None, - is_target_parser_func: Optional[IsParserFunc] = None) -> bool: +def write_annotation_images_from_path( + annotation_path: Path, + image_size: InputDataSize, + label_color_dict: Dict[str, RGB], + output_dir_path: Path, + output_image_extension: str = "png", + background_color: Optional[Any] = None, + is_target_parser_func: Optional[IsParserFunc] = None, +) -> bool: """ AnnoFabからダウンロードしたアノテーションzipファイル、またはそのzipを展開したディレクトリから、アノテーション情報を画像化します。 @@ -205,8 +222,13 @@ def write_annotation_images_from_path(annotation_path: Path, image_size: InputDa continue output_image_file = output_dir_path / f"{Path(parser.json_file_path).stem}.{output_image_extension}" - write_annotation_image(parser, image_size=image_size, label_color_dict=label_color_dict, - background_color=background_color, output_image_file=output_image_file) + write_annotation_image( + parser, + image_size=image_size, + label_color_dict=label_color_dict, + background_color=background_color, + output_image_file=output_image_file, + ) logger.debug(f"画像ファイル '{str(output_image_file)}' を生成しました。") count_created_image += 1 diff --git a/annofabcli/common/utils.py b/annofabcli/common/utils.py index 38c27fb2..6213adbb 100644 --- a/annofabcli/common/utils.py +++ b/annofabcli/common/utils.py @@ -19,14 +19,14 @@ logger = logging.getLogger(__name__) -T = TypeVar('T') # Can be anything +T = TypeVar("T") # Can be anything def read_lines(filepath: str) -> List[str]: """ファイルを行単位で読み込む。改行コードを除く""" with open(filepath) as f: lines = f.readlines() - return [e.rstrip('\r\n') for e in lines] + return [e.rstrip("\r\n") for e in lines] def read_lines_except_blank_line(filepath: str) -> List[str]: @@ -36,7 +36,7 @@ def read_lines_except_blank_line(filepath: str) -> List[str]: def _is_file_scheme(value: str): - return value.startswith('file://') + return value.startswith("file://") def load_logging_config(log_dir: str, logging_yaml_file: Optional[str] = None): @@ -52,7 +52,7 @@ def load_logging_config(log_dir: str, logging_yaml_file: Optional[str] = None): if logging_yaml_file is not None: if os.path.exists(logging_yaml_file): - with open(logging_yaml_file, encoding='utf-8') as f: + with open(logging_yaml_file, encoding="utf-8") as f: logging_config = yaml.safe_load(f) logging.config.dictConfig(logging_config) else: @@ -67,7 +67,7 @@ def set_default_logger(log_dir: str = ".log", log_filename: str = "annofabcli.lo """ デフォルトのロガーを設定する。パッケージ内のlogging.yamlを読み込む。 """ - data = pkgutil.get_data('annofabcli', 'data/logging.yaml') + data = pkgutil.get_data("annofabcli", "data/logging.yaml") if data is None: logger.warning("annofabcli/data/logging.yaml が読み込めませんでした") raise AnnofabCliException("annofabcli/data/logging.yaml が読み込めませんでした") @@ -99,7 +99,7 @@ def progress_msg(index: int, size: int): `1/100件目`という進捗率を表示する """ digit = len(str(size)) - str_format = f'{{:{digit}}} / {{:{digit}}} 件目' + str_format = f"{{:{digit}}} / {{:{digit}}} 件目" return str_format.format(index, size) @@ -115,7 +115,7 @@ def output_string(target: str, output: Optional[str] = None): print(target) else: Path(output).parent.mkdir(parents=True, exist_ok=True) - with open(output, mode='w', encoding='utf_8_sig') as f: + with open(output, mode="w", encoding="utf_8_sig") as f: f.write(target) @@ -135,12 +135,13 @@ def print_csv(df: pandas.DataFrame, output: Optional[str] = None, to_csv_kwargs: def print_id_list(id_list: List[Any], output: Optional[str]): - s = '\n'.join(id_list) + s = "\n".join(id_list) output_string(s, output) -def print_according_to_format(target: Any, arg_format: FormatArgument, output: Optional[str] = None, - csv_format: Optional[Dict[str, Any]] = None): +def print_according_to_format( + target: Any, arg_format: FormatArgument, output: Optional[str] = None, csv_format: Optional[Dict[str, Any]] = None +): """ コマンドライン引数 ``--format`` の値にしたがって、内容を出力する。 @@ -164,19 +165,19 @@ def print_according_to_format(target: Any, arg_format: FormatArgument, output: O annofabcli.utils.print_csv(df, output=output, to_csv_kwargs=csv_format) elif arg_format == FormatArgument.TASK_ID_LIST: - task_id_list = [e['task_id'] for e in target] + task_id_list = [e["task_id"] for e in target] print_id_list(task_id_list, output) elif arg_format == FormatArgument.INPUT_DATA_ID_LIST: - input_data_id_list = [e['input_data_id'] for e in target] + input_data_id_list = [e["input_data_id"] for e in target] print_id_list(input_data_id_list, output) elif arg_format == FormatArgument.USER_ID_LIST: - user_id_list = [e['user_id'] for e in target] + user_id_list = [e["user_id"] for e in target] print_id_list(user_id_list, output) elif arg_format == FormatArgument.INSPECTION_ID_LIST: - inspection_id_list = [e['inspection_id'] for e in target] + inspection_id_list = [e["inspection_id"] for e in target] print_id_list(inspection_id_list, output) @@ -190,7 +191,7 @@ def to_filename(s: str): ファイル名用の文字列 """ - return re.sub(r'[\\|/|:|?|.|"|<|>|\|]', '__', s) + return re.sub(r'[\\|/|:|?|.|"|<|>|\|]', "__", s) def is_file_scheme(str_value: str) -> bool: @@ -198,7 +199,7 @@ def is_file_scheme(str_value: str) -> bool: file schemaかどうか """ - return str_value.startswith('file://') + return str_value.startswith("file://") def get_file_scheme_path(str_value: str) -> Optional[str]: @@ -207,7 +208,7 @@ def get_file_scheme_path(str_value: str) -> Optional[str]: """ if is_file_scheme(str_value): - return str_value[len('file://'):] + return str_value[len("file://") :] else: return None @@ -256,6 +257,7 @@ def allow_404_error(function): リソースの存在確認などに利用する。 try-exceptを行う。また404 Errorが発生したときのエラーログを無効化する """ + def wrapped(*args, **kwargs): annofabapi_logger_level = logging.getLogger("annofabapi").level backoff_logger_level = logging.getLogger("backoff").level diff --git a/annofabcli/common/visualize.py b/annofabcli/common/visualize.py index d490bcef..59198061 100644 --- a/annofabcli/common/visualize.py +++ b/annofabcli/common/visualize.py @@ -7,8 +7,15 @@ import annofabapi import more_itertools -from annofabapi.models import (AnnotationSpecsHistory, InputData, Inspection, OrganizationMember, Task, - TaskHistoryShort, TaskPhase) +from annofabapi.models import ( + AnnotationSpecsHistory, + InputData, + Inspection, + OrganizationMember, + Task, + TaskHistoryShort, + TaskPhase, +) class MessageLocale(enum.Enum): @@ -30,8 +37,8 @@ def __init__(self, service: annofabapi.Resource, project_id: str): self.organization_name = self._get_organization_name_from_project_id(project_id) annotation_specs, _ = self.service.api.get_annotation_specs(project_id) - self.specs_labels: List[Dict[str, Any]] = annotation_specs['labels'] - self.specs_inspection_phrases: List[Dict[str, Any]] = annotation_specs['inspection_phrases'] + self.specs_labels: List[Dict[str, Any]] = annotation_specs["labels"] + self.specs_inspection_phrases: List[Dict[str, Any]] = annotation_specs["inspection_phrases"] @staticmethod def millisecond_to_hour(millisecond: int): @@ -39,16 +46,16 @@ def millisecond_to_hour(millisecond: int): @staticmethod def get_message(i18n_messages: Dict[str, Any], locale: MessageLocale) -> Optional[str]: - messages: List[Dict[str, Any]] = i18n_messages['messages'] + messages: List[Dict[str, Any]] = i18n_messages["messages"] dict_message = more_itertools.first_true(messages, pred=lambda e: e["lang"] == locale.value) if dict_message is not None: - return dict_message['message'] + return dict_message["message"] else: return None @staticmethod def add_properties_of_project(target: Dict[str, Any], project_title: str) -> Dict[str, Any]: - target['project_title'] = project_title + target["project_title"] = project_title return target def _add_user_info(self, target: Any): @@ -59,11 +66,11 @@ def _add_user_info(self, target: Any): if account_id is not None: member = self.get_organization_member_from_account_id(account_id) if member is not None: - user_id = member['user_id'] - username = member['username'] + user_id = member["user_id"] + username = member["username"] - target['user_id'] = user_id - target['username'] = username + target["user_id"] = user_id + target["username"] = username return target def get_organization_member_from_account_id(self, account_id: str) -> Optional[OrganizationMember]: @@ -78,6 +85,7 @@ def get_organization_member_from_account_id(self, account_id: str) -> Optional[O Returns: 組織メンバ """ + def update_organization_members(): self._organization_members = self.service.wrapper.get_all_organization_members(self.organization_name) @@ -106,22 +114,24 @@ def _get_organization_name_from_project_id(self, project_id: str) -> str: return organization["organization_name"] def get_phrase_name(self, phrase_id, locale: MessageLocale) -> Optional[str]: - phrase: Optional[Dict[str, Any]] = more_itertools.first_true(self.specs_inspection_phrases, - pred=lambda e: e['id'] == phrase_id) + phrase: Optional[Dict[str, Any]] = more_itertools.first_true( + self.specs_inspection_phrases, pred=lambda e: e["id"] == phrase_id + ) if phrase is None: return None - return self.get_message(phrase['text'], locale) + return self.get_message(phrase["text"], locale) def get_label_name(self, label_id: str, locale: MessageLocale) -> Optional[str]: - label = more_itertools.first_true(self.specs_labels, pred=lambda e: e['label_id'] == label_id) + label = more_itertools.first_true(self.specs_labels, pred=lambda e: e["label_id"] == label_id) if label is None: return None - return self.get_message(label['label_name'], locale) + return self.get_message(label["label_name"], locale) def add_properties_to_annotation_specs_history( - self, annotation_specs_history: AnnotationSpecsHistory) -> AnnotationSpecsHistory: + self, annotation_specs_history: AnnotationSpecsHistory + ) -> AnnotationSpecsHistory: """ アノテーション仕様の履歴に、以下のキーを追加する. user_id @@ -135,8 +145,9 @@ def add_properties_to_annotation_specs_history( """ return self._add_user_info(annotation_specs_history) - def add_properties_to_inspection(self, inspection: Inspection, detail: Optional[Dict[str, - Any]] = None) -> Inspection: + def add_properties_to_inspection( + self, inspection: Inspection, detail: Optional[Dict[str, Any]] = None + ) -> Inspection: """ 検査コメントに、以下のキーを追加する. commenter_user_id @@ -153,6 +164,7 @@ def add_properties_to_inspection(self, inspection: Inspection, detail: Optional[ Returns: """ + def add_commenter_info(): commenter_user_id = None commenter_username = None @@ -161,18 +173,18 @@ def add_commenter_info(): if commenter_account_id is not None: member = self.get_organization_member_from_account_id(commenter_account_id) if member is not None: - commenter_user_id = member['user_id'] - commenter_username = member['username'] + commenter_user_id = member["user_id"] + commenter_username = member["username"] - inspection['commenter_user_id'] = commenter_user_id - inspection['commenter_username'] = commenter_username + inspection["commenter_user_id"] = commenter_user_id + inspection["commenter_username"] = commenter_username add_commenter_info() - inspection['phrase_names_en'] = [self.get_phrase_name(e, MessageLocale.EN) for e in inspection['phrases']] - inspection['phrase_names_ja'] = [self.get_phrase_name(e, MessageLocale.JA) for e in inspection['phrases']] + inspection["phrase_names_en"] = [self.get_phrase_name(e, MessageLocale.EN) for e in inspection["phrases"]] + inspection["phrase_names_ja"] = [self.get_phrase_name(e, MessageLocale.JA) for e in inspection["phrases"]] - inspection['label_name_en'] = self.get_label_name(inspection['label_id'], MessageLocale.EN) - inspection['label_name_ja'] = self.get_label_name(inspection['label_id'], MessageLocale.JA) + inspection["label_name_en"] = self.get_label_name(inspection["label_id"], MessageLocale.EN) + inspection["label_name_ja"] = self.get_label_name(inspection["label_id"], MessageLocale.JA) if detail is not None: inspection.update(detail) @@ -198,15 +210,17 @@ def add_properties_to_task(self, task: Task) -> Task: """ self._add_user_info(task) - task['worktime_hour'] = self.millisecond_to_hour(task['work_time_span']) + task["worktime_hour"] = self.millisecond_to_hour(task["work_time_span"]) - histories = [self._add_user_info(e) for e in task['histories_by_phase']] - task['histories_by_phase'] = histories + histories = [self._add_user_info(e) for e in task["histories_by_phase"]] + task["histories_by_phase"] = histories - task['number_of_rejections_by_inspection'] = self.get_number_of_rejections_by_phase( - TaskPhase.INSPECTION, histories) - task['number_of_rejections_by_acceptance'] = self.get_number_of_rejections_by_phase( - TaskPhase.ACCEPTANCE, histories) + task["number_of_rejections_by_inspection"] = self.get_number_of_rejections_by_phase( + TaskPhase.INSPECTION, histories + ) + task["number_of_rejections_by_acceptance"] = self.get_number_of_rejections_by_phase( + TaskPhase.ACCEPTANCE, histories + ) return task @@ -225,7 +239,7 @@ def add_properties_to_input_data(input_data: InputData, task_id_list: List[str]) 入力データ情報 """ - input_data['parent_task_id_list'] = task_id_list + input_data["parent_task_id_list"] = task_id_list return input_data @staticmethod @@ -244,10 +258,10 @@ def get_number_of_rejections_by_phase(phase: TaskPhase, task_histories: List[Tas rejections_by_phase = 0 for i, history in enumerate(task_histories): - if history['phase'] != phase.value: + if history["phase"] != phase.value: continue - if i + 1 < len(task_histories) and task_histories[i + 1]['phase'] == TaskPhase.ANNOTATION.value: + if i + 1 < len(task_histories) and task_histories[i + 1]["phase"] == TaskPhase.ANNOTATION.value: rejections_by_phase += 1 return rejections_by_phase diff --git a/annofabcli/deprecated_register_annotation_from_full_zip.py b/annofabcli/deprecated_register_annotation_from_full_zip.py index c794d3b5..66751358 100644 --- a/annofabcli/deprecated_register_annotation_from_full_zip.py +++ b/annofabcli/deprecated_register_annotation_from_full_zip.py @@ -35,8 +35,8 @@ def __init__(self, service: annofabapi.Resource, facade: AnnofabApiFacade): # AnnofabAPI をカスタマイズ def get_annotations_for_editor(self, project_id: str, task_id: str, input_data_id: str): - url_path = f'/projects/{project_id}/tasks/{task_id}/inputs/{input_data_id}/annotation' - http_method = 'GET' + url_path = f"/projects/{project_id}/tasks/{task_id}/inputs/{input_data_id}/annotation" + http_method = "GET" keyword_params: Dict[str, Any] = {} return self.service.api._request_wrapper(http_method, url_path, **keyword_params) @@ -56,8 +56,10 @@ def draw_annotation_list(annotation_list: List[Annotation], draw: PIL.ImageDraw. color = (255, 255, 255, 255) data_type = data["_type"] if data_type == "BoundingBox": - xy = [(data["left_top"]["x"], data["left_top"]["y"]), - (data["right_bottom"]["x"], data["right_bottom"]["y"])] + xy = [ + (data["left_top"]["x"], data["left_top"]["y"]), + (data["right_bottom"]["x"], data["right_bottom"]["y"]), + ] draw.rectangle(xy, fill=color) elif data_type == "Points": @@ -72,8 +74,15 @@ def draw_annotation_list(annotation_list: List[Annotation], draw: PIL.ImageDraw. return draw - def update_annotation_with_image(self, project_id: str, task_id: str, input_data_id: str, - image_file_list: List[Any], account_id: str, filter_details: FilterDetailsFunc): + def update_annotation_with_image( + self, + project_id: str, + task_id: str, + input_data_id: str, + image_file_list: List[Any], + account_id: str, + filter_details: FilterDetailsFunc, + ): """ 塗りつぶしアノテーションを登録する。他のアノテーションが変更されないようにする。 アノテーションを登録できる状態であること @@ -127,13 +136,19 @@ def update_annotation_with_image(self, project_id: str, task_id: str, input_data "task_id": task_id, "input_data_id": input_data_id, "details": details, - "updated_datetime": old_annotations["updated_datetime"] + "updated_datetime": old_annotations["updated_datetime"], } return self.service.api.put_annotation(project_id, task_id, input_data_id, request_body=request_body)[0] - def write_segmentation_image(self, input_data: Dict[str, Any], label: str, label_id: str, tmp_image_path: Path, - input_data_size: InputDataSize): + def write_segmentation_image( + self, + input_data: Dict[str, Any], + label: str, + label_id: str, + tmp_image_path: Path, + input_data_size: InputDataSize, + ): image = PIL.Image.new(mode="RGBA", size=input_data_size, color=(0, 0, 0, 0)) draw = PIL.ImageDraw.Draw(image) @@ -141,8 +156,10 @@ def write_segmentation_image(self, input_data: Dict[str, Any], label: str, label # labelで絞り込み annotation_list = [e for e in input_data["detail"] if e["label_id"] == label_id] if len(annotation_list) == 0: - logger.info(f"{input_data['task_id']}, {input_data['input_data_id']} に " - f"label:{label}, label_id:{label_id} のアノテーションがない") + logger.info( + f"{input_data['task_id']}, {input_data['input_data_id']} に " + f"label:{label}, label_id:{label_id} のアノテーションがない" + ) return False # アノテーションを描画する @@ -153,8 +170,13 @@ def write_segmentation_image(self, input_data: Dict[str, Any], label: str, label logger.info(f"{str(tmp_image_path)} の生成完了") return True - def write_segmentation_image_for_labels(self, labels: List[Dict[str, str]], input_data: Dict[str, Any], - default_input_data_size: InputDataSize, tmp_image_dir: Path): + def write_segmentation_image_for_labels( + self, + labels: List[Dict[str, str]], + input_data: Dict[str, Any], + default_input_data_size: InputDataSize, + tmp_image_dir: Path, + ): """ ラベルごとに、セマンティック画像ファイルを出力する。 Args: @@ -174,18 +196,29 @@ def write_segmentation_image_for_labels(self, labels: List[Dict[str, str]], inpu tmp_image_path = tmp_image_dir / f"{label}.png" - result = self.write_segmentation_image(input_data=input_data, label=label, label_id=label_id, - tmp_image_path=tmp_image_path, - input_data_size=default_input_data_size) + result = self.write_segmentation_image( + input_data=input_data, + label=label, + label_id=label_id, + tmp_image_path=tmp_image_path, + input_data_size=default_input_data_size, + ) if result: image_file_list.append({"path": str(tmp_image_path), "label_id": label_id}) return image_file_list - def register_raster_annotation_from_polygon(self, annotation_dir: str, default_input_data_size: InputDataSize, - tmp_dir: str, labels: List[Dict[str, str]], project_id: str, - task_id_list: List[str], filter_details: FilterDetailsFunc): + def register_raster_annotation_from_polygon( + self, + annotation_dir: str, + default_input_data_size: InputDataSize, + tmp_dir: str, + labels: List[Dict[str, str]], + project_id: str, + task_id_list: List[str], + filter_details: FilterDetailsFunc, + ): annotation_dir_path = Path(annotation_dir) tmp_dir_path = Path(tmp_dir) @@ -207,8 +240,9 @@ def register_raster_annotation_from_polygon(self, annotation_dir: str, default_i tmp_image_dir = tmp_dir_path / task_dir.name / input_data_json_path.stem try: - image_file_list = self.write_segmentation_image_for_labels(labels, input_data, - default_input_data_size, tmp_image_dir) + image_file_list = self.write_segmentation_image_for_labels( + labels, input_data, default_input_data_size, tmp_image_dir + ) except Exception as e: logger.exception(e) @@ -233,8 +267,14 @@ def register_raster_annotation_from_polygon(self, annotation_dir: str, default_i try: # アノテーションの登録 - self.update_annotation_with_image(project_id, task_id, input_data_id, account_id=account_id, - image_file_list=image_file_list, filter_details=filter_details) + self.update_annotation_with_image( + project_id, + task_id, + input_data_id, + account_id=account_id, + image_file_list=image_file_list, + filter_details=filter_details, + ) self.facade.change_to_break_phase(project_id, task_id, account_id) @@ -274,38 +314,40 @@ def filter_details(annotation: Annotation) -> bool: # vehicle "030bc859-4933-4bec-baa0-18fc80fb1eea", # motorcycle - "4bc53fa5-bb2e-44a5-adb2-04c76d87bfde" + "4bc53fa5-bb2e-44a5-adb2-04c76d87bfde", ] label_id = annotation["label_id"] # 除外するlabel_idにマッチしたらFalse return label_id not in exclude_label_ids - labels = [{ - "label": "vehicle", - "label_id": "030bc859-4933-4bec-baa0-18fc80fb1eea" - }, { - "label": "motorcycle", - "label_id": "4bc53fa5-bb2e-44a5-adb2-04c76d87bfde" - }] + labels = [ + {"label": "vehicle", "label_id": "030bc859-4933-4bec-baa0-18fc80fb1eea"}, + {"label": "motorcycle", "label_id": "4bc53fa5-bb2e-44a5-adb2-04c76d87bfde"}, + ] task_id_list = annofabcli.utils.read_lines_except_blank_line(args.task_id_file) - self.register_raster_annotation_from_polygon(annotation_dir=args.annotation_dir, - default_input_data_size=default_input_data_size, - tmp_dir=args.tmp_dir, labels=labels, project_id=args.project_id, - filter_details=filter_details, task_id_list=task_id_list) + self.register_raster_annotation_from_polygon( + annotation_dir=args.annotation_dir, + default_input_data_size=default_input_data_size, + tmp_dir=args.tmp_dir, + labels=labels, + project_id=args.project_id, + filter_details=filter_details, + task_id_list=task_id_list, + ) def parse_args(parser: argparse.ArgumentParser): - parser.add_argument('--annotation_dir', type=str, required=True, help='アノテーションFull zipを展開したディレクトリのパス') + parser.add_argument("--annotation_dir", type=str, required=True, help="アノテーションFull zipを展開したディレクトリのパス") - parser.add_argument('--input_data_size', type=str, required=True, help='入力データ画像のサイズ。{width}x{height}。ex. 1280x720') + parser.add_argument("--input_data_size", type=str, required=True, help="入力データ画像のサイズ。{width}x{height}。ex. 1280x720") - parser.add_argument('--tmp_dir', type=str, required=True, help='temporaryディレクトリのパス') + parser.add_argument("--tmp_dir", type=str, required=True, help="temporaryディレクトリのパス") - parser.add_argument('--project_id', type=str, required=True, help='塗りつぶしv2アノテーションを登録するプロジェクトのproject_id') + parser.add_argument("--project_id", type=str, required=True, help="塗りつぶしv2アノテーションを登録するプロジェクトのproject_id") - parser.add_argument('--task_id_file', type=str, required=True, help='task_idの一覧が記載されたファイル') + parser.add_argument("--task_id_file", type=str, required=True, help="task_idの一覧が記載されたファイル") def main(args): @@ -316,8 +358,9 @@ def main(args): if __name__ == "__main__": global_parser = argparse.ArgumentParser( - description="deprecated: 矩形/ポリゴンアノテーションを、塗りつぶしv2アノテーションとして登録する。" - "注意:対象プロジェクトに合わせてスクリプトを修正すること。そのままでは実行できに。", parents=[annofabcli.common.cli.create_parent_parser()]) + description="deprecated: 矩形/ポリゴンアノテーションを、塗りつぶしv2アノテーションとして登録する。" "注意:対象プロジェクトに合わせてスクリプトを修正すること。そのままでは実行できに。", + parents=[annofabcli.common.cli.create_parent_parser()], + ) parse_args(global_parser) diff --git a/annofabcli/experimental/list_labor_worktime.py b/annofabcli/experimental/list_labor_worktime.py index 98d6be03..6993f2b3 100644 --- a/annofabcli/experimental/list_labor_worktime.py +++ b/annofabcli/experimental/list_labor_worktime.py @@ -19,17 +19,16 @@ logger = logging.getLogger(__name__) -class Database(): +class Database: def __init__(self, annofab_service: annofabapi.Resource, project_id: str, organization_id: str): self.annofab_service = annofab_service self.project_id = project_id self.organization_id = organization_id def get_labor_control(self) -> List[Dict[str, Any]]: - labor_control_df = self.annofab_service.api.get_labor_control({ - "organization_id": self.organization_id, - "project_id": self.project_id - })[0] + labor_control_df = self.annofab_service.api.get_labor_control( + {"organization_id": self.organization_id, "project_id": self.project_id} + )[0] return labor_control_df def get_account_statistics(self) -> List[Dict[str, Any]]: @@ -68,26 +67,32 @@ def create_labor_control_df(self): if l["account_id"] == None: continue if self.user_id_search_list == None: - labor_control_list.append({ - "account_id": l["account_id"], - "date": l["date"], - "aw_plans": aw_plans, - "aw_results": aw_results, - "username": self._get_username(l["account_id"]), - "af_time": 0.0 - }) - else: - user_id_bool_list = [user_id_search in self._get_user_id(l["account_id"]).upper() for user_id_search in - self.user_id_search_list] - if True in user_id_bool_list: - labor_control_list.append({ + labor_control_list.append( + { "account_id": l["account_id"], "date": l["date"], "aw_plans": aw_plans, "aw_results": aw_results, "username": self._get_username(l["account_id"]), - "af_time": 0.0 - }) + "af_time": 0.0, + } + ) + else: + user_id_bool_list = [ + user_id_search in self._get_user_id(l["account_id"]).upper() + for user_id_search in self.user_id_search_list + ] + if True in user_id_bool_list: + labor_control_list.append( + { + "account_id": l["account_id"], + "date": l["date"], + "aw_plans": aw_plans, + "aw_results": aw_results, + "username": self._get_username(l["account_id"]), + "af_time": 0.0, + } + ) return labor_control_list @@ -99,27 +104,29 @@ def create_account_statistics_df(self): all_histories = [] for account_info in account_statistics: - account_id = account_info['account_id'] - histories = account_info['histories'] + account_id = account_info["account_id"] + histories = account_info["histories"] if account_id == None: continue elif self.user_id_search_list == None: for history in histories: - history['af_time'] = annofabcli.utils.isoduration_to_minute(history['worktime']) - history['account_id'] = account_id - history['username'] = self._get_username(account_id) + history["af_time"] = annofabcli.utils.isoduration_to_minute(history["worktime"]) + history["account_id"] = account_id + history["username"] = self._get_username(account_id) history["aw_plans"] = 0.0 history["aw_results"] = 0.0 all_histories.extend(histories) else: - user_id_bool_list = [user_id_search in self._get_user_id(account_id).upper().upper() for user_id_search - in self.user_id_search_list] + user_id_bool_list = [ + user_id_search in self._get_user_id(account_id).upper().upper() + for user_id_search in self.user_id_search_list + ] if True in user_id_bool_list: for history in histories: - history['af_time'] = annofabcli.utils.isoduration_to_minute(history['worktime']) - history['account_id'] = account_id - history['username'] = self._get_username(account_id) + history["af_time"] = annofabcli.utils.isoduration_to_minute(history["worktime"]) + history["account_id"] = account_id + history["username"] = self._get_username(account_id) history["aw_plans"] = 0.0 history["aw_results"] = 0.0 @@ -135,8 +142,10 @@ def create_afaw_time_df(self) -> Tuple[List[Any], List[Any]]: for labor_control in labor_control_df: for account_statistics in account_statistics_df: - if account_statistics["account_id"] == labor_control["account_id"] and account_statistics["date"] == \ - labor_control["date"]: + if ( + account_statistics["account_id"] == labor_control["account_id"] + and account_statistics["date"] == labor_control["date"] + ): labor_control["af_time"] = account_statistics["af_time"] labor_control["username"] = account_statistics["username"] if not account_statistics["username"] in username_list: @@ -175,8 +184,8 @@ def list_labor_worktime(self, project_id: str, user_id_list: List[str]): def main(self): args = self.args - start_date = datetime.datetime.strptime(args.start_date, '%Y-%m-%d').date() - end_date = datetime.datetime.strptime(args.end_date, '%Y-%m-%d').date() + start_date = datetime.datetime.strptime(args.start_date, "%Y-%m-%d").date() + end_date = datetime.datetime.strptime(args.end_date, "%Y-%m-%d").date() date_list = date_range(start_date, end_date) user_id_list = args.user_id @@ -187,9 +196,9 @@ def main(self): afaw_time_df, project_members = self.list_labor_worktime(project_id, user_id_list) afaw_time_list.append(afaw_time_df) project_members_list.extend(project_members) - print_time_list, print_total_time = print_time_list_from_work_time_list(list(set(project_members_list)), - afaw_time_list, - date_list) + print_time_list, print_total_time = print_time_list_from_work_time_list( + list(set(project_members_list)), afaw_time_list, date_list + ) output_lines: List[str] = [] output_lines.append(f"Start: , {start_date}, End: , {end_date}") @@ -212,11 +221,21 @@ def main(args): def parse_args(parser: argparse.ArgumentParser): argument_parser = ArgumentParser(parser) parser.add_argument( - '-p', '--project_id', type=str, required=True, nargs='+', - help="集計対象のプロジェクトのproject_idを指定します。複数指定した場合は合計値を出力します。") + "-p", + "--project_id", + type=str, + required=True, + nargs="+", + help="集計対象のプロジェクトのproject_idを指定します。複数指定した場合は合計値を出力します。", + ) parser.add_argument( - '-u', '--user_id', type=str, nargs='+', default=None, help='集計対象のユーザのuser_idに部分一致するものを集計します。' - '指定しない場合は、プロジェクトメンバが指定されます。') + "-u", + "--user_id", + type=str, + nargs="+", + default=None, + help="集計対象のユーザのuser_idに部分一致するものを集計します。" "指定しない場合は、プロジェクトメンバが指定されます。", + ) parser.add_argument("--start_date", type=str, required=True, help="集計開始日(%%Y-%%m-%%d)") parser.add_argument("--end_date", type=str, required=True, help="集計終了日(%%Y-%%m-%%d)") @@ -228,7 +247,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "list_labor_worktime" subcommand_help = "労務管理画面の作業時間を出力します。" - description = ("作業者ごとに、「作業者が入力した実績時間」と「AnnoFabが集計した作業時間」の差分を出力します。") + description = "作業者ごとに、「作業者が入力した実績時間」と「AnnoFabが集計した作業時間」の差分を出力します。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description) parse_args(parser) diff --git a/annofabcli/experimental/subcommand_experimental.py b/annofabcli/experimental/subcommand_experimental.py index 9f8a8a5c..40b111b0 100644 --- a/annofabcli/experimental/subcommand_experimental.py +++ b/annofabcli/experimental/subcommand_experimental.py @@ -7,7 +7,7 @@ def parse_args(parser: argparse.ArgumentParser): - subparsers = parser.add_subparsers(dest='subcommand_name') + subparsers = parser.add_subparsers(dest="subcommand_name") # サブコマンドの定義 annofabcli.experimental.list_labor_worktime.add_parser(subparsers) # type: ignore diff --git a/annofabcli/experimental/utils.py b/annofabcli/experimental/utils.py index 8e47eb12..c982fe20 100644 --- a/annofabcli/experimental/utils.py +++ b/annofabcli/experimental/utils.py @@ -78,19 +78,28 @@ def print_time_list_from_work_time_list(username_list: list, work_time_lists: li new_footer_item_list.append(round(username_footer_list[username]["af_time"], 2)) total_af_time += username_footer_list[username]["af_time"] total_diff = round(username_footer_list[username]["aw_results"] - username_footer_list[username]["af_time"], 2) - total_diff_per = round(total_diff / username_footer_list[username]["aw_results"], 2) \ - if (username_footer_list[username]["aw_results"] != 0.0) else 0.0 + total_diff_per = ( + round(total_diff / username_footer_list[username]["aw_results"], 2) + if (username_footer_list[username]["aw_results"] != 0.0) + else 0.0 + ) new_footer_item_list.append(total_diff) new_footer_item_list.append(total_diff_per) print_time_list.append(new_footer_item_list) - return print_time_list, {"total_aw_results": round(total_aw_results, 2), - "total_aw_plans": round(total_aw_plans, 2), - "total_af_time": round(total_af_time, 2), - "total_diff": round(total_aw_results - total_af_time, 2), - "total_diff_per": (round((total_aw_results - total_af_time) / total_aw_results, 2) if ( - total_aw_results != 0.00) else 0.00)} + return ( + print_time_list, + { + "total_aw_results": round(total_aw_results, 2), + "total_aw_plans": round(total_aw_plans, 2), + "total_af_time": round(total_af_time, 2), + "total_diff": round(total_aw_results - total_af_time, 2), + "total_diff_per": ( + round((total_aw_results - total_af_time) / total_aw_results, 2) if (total_aw_results != 0.00) else 0.00 + ), + }, + ) def print_time_list_csv(print_time_list: list) -> None: diff --git a/annofabcli/filesystem/write_annotation_image.py b/annofabcli/filesystem/write_annotation_image.py index ca62e21a..70b39424 100644 --- a/annofabcli/filesystem/write_annotation_image.py +++ b/annofabcli/filesystem/write_annotation_image.py @@ -23,20 +23,24 @@ class WriteAnnotationImage: @staticmethod - def create_is_target_parser_func(task_status_complete: bool = False, - task_id_list: Optional[List[str]] = None) -> IsParserFunc: + def create_is_target_parser_func( + task_status_complete: bool = False, task_id_list: Optional[List[str]] = None + ) -> IsParserFunc: def is_target_parser(parser: SimpleAnnotationParser) -> bool: simple_annotation = parser.parse() if task_status_complete: if simple_annotation.task_status != TaskStatus.COMPLETE: - logger.debug(f"task_statusがcompleteでない( {simple_annotation.task_status.value})ため、" - f"{simple_annotation.task_id}, {simple_annotation.input_data_name} はスキップします。") + logger.debug( + f"task_statusがcompleteでない( {simple_annotation.task_status.value})ため、" + f"{simple_annotation.task_id}, {simple_annotation.input_data_name} はスキップします。" + ) return False if task_id_list is not None and len(task_id_list) > 0: if simple_annotation.task_id not in task_id_list: logger.debug( - f"画像化対象外のタスク {simple_annotation.task_id} であるため、 {simple_annotation.input_data_name} はスキップします。") + f"画像化対象外のタスク {simple_annotation.task_id} であるため、 {simple_annotation.input_data_name} はスキップします。" + ) return False return True @@ -61,12 +65,15 @@ def main(self, args): is_target_parser_func = self.create_is_target_parser_func(args.task_status_complete, task_id_list) # 画像生成 - result = write_annotation_images_from_path(annotation_path, image_size=image_size, - label_color_dict=label_color_dict, - output_dir_path=Path(args.output_dir), - output_image_extension=args.image_extension, - background_color=args.background_color, - is_target_parser_func=is_target_parser_func) + result = write_annotation_images_from_path( + annotation_path, + image_size=image_size, + label_color_dict=label_color_dict, + output_dir_path=Path(args.output_dir), + output_image_extension=args.image_extension, + background_color=args.background_color, + is_target_parser_func=is_target_parser_func, + ) if not result: logger.error(f"'{annotation_path}' のアノテーション情報の画像化に失敗しました。") @@ -78,34 +85,41 @@ def main(args): def parse_args(parser: argparse.ArgumentParser): argument_parser = ArgumentParser(parser) - parser.add_argument('--annotation', type=str, required=True, help='アノテーションzip、またはzipを展開したディレクトリ') + parser.add_argument("--annotation", type=str, required=True, help="アノテーションzip、またはzipを展開したディレクトリ") - parser.add_argument('--image_size', type=str, required=True, help='入力データ画像のサイズ。{width}x{height}。ex) 1280x720') + parser.add_argument("--image_size", type=str, required=True, help="入力データ画像のサイズ。{width}x{height}。ex) 1280x720") parser.add_argument( - '--label_color', type=str, required=True, + "--label_color", + type=str, + required=True, help='label_nameとRGBの関係をJSON形式で指定します。ex) `{"dog":[255,128,64], "cat":[0,0,255]}`' - '`file://`を先頭に付けると、JSON形式のファイルを指定できます。') + "`file://`を先頭に付けると、JSON形式のファイルを指定できます。", + ) - parser.add_argument('--output_dir', type=str, required=True, help='出力ディレクトリのパス') + parser.add_argument("--output_dir", type=str, required=True, help="出力ディレクトリのパス") - parser.add_argument('--image_extension', type=str, default="png", help='出力画像の拡張子') + parser.add_argument("--image_extension", type=str, default="png", help="出力画像の拡張子") parser.add_argument( - '--background_color', + "--background_color", type=str, help=( - 'アノテーションの画像の背景色を指定します。' + "アノテーションの画像の背景色を指定します。" 'ex): "rgb(173, 216, 230)", "lightgrey", "#add8e6" ' - '[ImageColor Module](https://hhsprings.bitbucket.io/docs/programming/examples/python/PIL/ImageColor.html) がサポートする文字列を利用できます。' # noqa: E501 - '指定しない場合は、黒(rgb(0,0,0))になります。')) + "[ImageColor Module](https://hhsprings.bitbucket.io/docs/programming/examples/python/PIL/ImageColor.html) がサポートする文字列を利用できます。" # noqa: E501 + "指定しない場合は、黒(rgb(0,0,0))になります。" + ), + ) - parser.add_argument('--task_status_complete', action="store_true", help='taskのstatusがcompleteの場合のみ画像を生成します。') + parser.add_argument("--task_status_complete", action="store_true", help="taskのstatusがcompleteの場合のみ画像を生成します。") argument_parser.add_task_id( - required=False, help_message=('画像化するタスクのtask_idを指定します。' - '指定しない場合、すべてのタスクが画像化されます。' - '`file://`を先頭に付けると、task_idの一覧が記載されたファイルを指定できます。')) + required=False, + help_message=( + "画像化するタスクのtask_idを指定します。" "指定しない場合、すべてのタスクが画像化されます。" "`file://`を先頭に付けると、task_idの一覧が記載されたファイルを指定できます。" + ), + ) parser.set_defaults(subcommand_func=main) @@ -115,7 +129,7 @@ def add_parser(subparsers: argparse._SubParsersAction): subcommand_help = "アノテーションzipを展開したディレクトリから、アノテーションの画像(Semantic Segmentation用)を生成する。" - description = ("アノテーションzipを展開したディレクトリから、アノテーションの画像(Semantic Segmentation用)を生成する。" "矩形、ポリゴン、塗りつぶし、塗りつぶしv2が対象。") + description = "アノテーションzipを展開したディレクトリから、アノテーションの画像(Semantic Segmentation用)を生成する。" "矩形、ポリゴン、塗りつぶし、塗りつぶしv2が対象。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description) parse_args(parser) diff --git a/annofabcli/input_data/delete_input_data.py b/annofabcli/input_data/delete_input_data.py index 564ef205..509ae720 100644 --- a/annofabcli/input_data/delete_input_data.py +++ b/annofabcli/input_data/delete_input_data.py @@ -16,14 +16,16 @@ class DeleteInputData(AbstractCommandLineInterface): """ タスクに使われていない入力データを削除する。 """ + @annofabcli.utils.allow_404_error def get_input_data(self, project_id: str, input_data_id: str) -> Dict[str, Any]: input_data, _ = self.service.api.get_input_data(project_id, input_data_id) return input_data def confirm_delete_input_data(self, input_data_id: str, input_data_name: str) -> bool: - message_for_confirm = (f"入力データ(input_data_id='{input_data_id}', " - f"input_data_name='{input_data_name}') を削除しますか?") + message_for_confirm = ( + f"入力データ(input_data_id='{input_data_id}', " f"input_data_name='{input_data_name}') を削除しますか?" + ) return self.confirm_processing(message_for_confirm) def delete_input_data(self, project_id: str, input_data_id: str): @@ -33,12 +35,14 @@ def delete_input_data(self, project_id: str, input_data_id: str): return False task_list = self.service.wrapper.get_all_tasks(project_id, query_params={"input_data_ids": input_data_id}) - input_data_name = input_data['input_data_name'] + input_data_name = input_data["input_data_name"] if len(task_list) > 0: task_id_list = [e["task_id"] for e in task_list] - logger.info(f"入力データ(input_data_id='{input_data_id}', " - f"input_data_name='{input_data_name}')はタスクに使われているので、削除しません。" - f"task_id_list='{task_id_list}'") + logger.info( + f"入力データ(input_data_id='{input_data_id}', " + f"input_data_name='{input_data_name}')はタスクに使われているので、削除しません。" + f"task_id_list='{task_id_list}'" + ) return False if not self.confirm_delete_input_data(input_data_id, input_data_name): @@ -87,9 +91,14 @@ def parse_args(parser: argparse.ArgumentParser): argument_parser = ArgumentParser(parser) argument_parser.add_project_id() - parser.add_argument('-i', '--input_data_id', type=str, required=True, nargs='+', - help='削除対象の入力データのinput_data_idを指定します。' - '`file://`を先頭に付けると、input_data_idの一覧が記載されたファイルを指定できます。') + parser.add_argument( + "-i", + "--input_data_id", + type=str, + required=True, + nargs="+", + help="削除対象の入力データのinput_data_idを指定します。" "`file://`を先頭に付けると、input_data_idの一覧が記載されたファイルを指定できます。", + ) parser.set_defaults(subcommand_func=main) @@ -97,7 +106,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "delete" subcommand_help = "入力データを削除します。" - description = ("入力データを削除します。ただし、タスクに使われている入力データは削除できません。") + description = "入力データを削除します。ただし、タスクに使われている入力データは削除できません。" epilog = "オーナロールを持つユーザで実行してください。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog) diff --git a/annofabcli/input_data/list_input_data.py b/annofabcli/input_data/list_input_data.py index 623633a0..f34f312c 100644 --- a/annofabcli/input_data/list_input_data.py +++ b/annofabcli/input_data/list_input_data.py @@ -28,6 +28,7 @@ class InputDataBatchQuery: """ 入力データをバッチ単位(段階的に)に取得する。 """ + first: str last: str days: int @@ -37,11 +38,12 @@ def str_to_datetime(d: str) -> datetime.datetime: """ 文字列 `YYYY-MM-DDD` をdatetime.datetimeに変換する。 """ - return datetime.datetime.strptime(d, '%Y-%m-%d') + return datetime.datetime.strptime(d, "%Y-%m-%d") -def create_datetime_range_list(first_datetime: datetime.datetime, last_datetime: datetime.datetime, - days: int) -> List[DatetimeRange]: +def create_datetime_range_list( + first_datetime: datetime.datetime, last_datetime: datetime.datetime, days: int +) -> List[DatetimeRange]: datetime_list: List[DatetimeRange] = [] datetime_list.append((None, first_datetime)) @@ -78,8 +80,8 @@ def _find_task_id_list(task_list: List[Task], input_data_id: str) -> List[str]: """ task_id_list = [] for task in task_list: - if input_data_id in task['input_data_id_list']: - task_id_list.append(task['task_id']) + if input_data_id in task["input_data_id_list"]: + task_id_list.append(task["task_id"]) return task_id_list @annofabcli.utils.allow_404_error @@ -125,15 +127,17 @@ def add_details_to_input_data_list(self, project_id: str, input_data_list: List[ chunk_size = MAX_URL_QUERY_LENGTH // average_input_data_id_length initial_index = 0 while True: - sub_input_data_list = input_data_list[initial_index:initial_index + chunk_size] - sub_input_data_id_list = [e['input_data_id'] for e in sub_input_data_list] + sub_input_data_list = input_data_list[initial_index : initial_index + chunk_size] + sub_input_data_id_list = [e["input_data_id"] for e in sub_input_data_list] str_input_data_id_list = ",".join(sub_input_data_id_list) encoded_input_data_id_list = urllib.parse.quote(str_input_data_id_list) if len(encoded_input_data_id_list) > MAX_URL_QUERY_LENGTH: - differential_length = (len(encoded_input_data_id_list) - MAX_URL_QUERY_LENGTH) + differential_length = len(encoded_input_data_id_list) - MAX_URL_QUERY_LENGTH decreasing_size = (differential_length // average_input_data_id_length) + 1 - logger.debug(f"chunk_sizeを {chunk_size} から、{chunk_size - decreasing_size} に減らした. " - f"len(encoded_input_data_id_list) = {len(encoded_input_data_id_list)}") + logger.debug( + f"chunk_sizeを {chunk_size} から、{chunk_size - decreasing_size} に減らした. " + f"len(encoded_input_data_id_list) = {len(encoded_input_data_id_list)}" + ) chunk_size = chunk_size - decreasing_size if chunk_size <= 0: chunk_size = 1 @@ -141,13 +145,14 @@ def add_details_to_input_data_list(self, project_id: str, input_data_list: List[ continue logger.debug(f"input_data_list[{initial_index}:{initial_index+chunk_size}] を使用しているタスクを取得する。") - task_list = self.service.wrapper.get_all_tasks(project_id, - query_params={'input_data_ids': str_input_data_id_list}) + task_list = self.service.wrapper.get_all_tasks( + project_id, query_params={"input_data_ids": str_input_data_id_list} + ) for input_data in sub_input_data_list: # input_data_idで絞り込んでいるが、大文字小文字を区別しない。 # したがって、確認のため `_find_task_id_list`を実行する - task_id_list = self._find_task_id_list(task_list, input_data['input_data_id']) + task_id_list = self._find_task_id_list(task_list, input_data["input_data_id"]) self.visualize.add_properties_to_input_data(input_data, task_id_list) initial_index = initial_index + chunk_size @@ -156,9 +161,13 @@ def add_details_to_input_data_list(self, project_id: str, input_data_list: List[ return input_data_list - def get_input_data_list(self, project_id: str, input_data_id_list: Optional[List[str]] = None, - input_data_query: Optional[Dict[str, - Any]] = None, add_details: bool = False) -> List[InputData]: + def get_input_data_list( + self, + project_id: str, + input_data_id_list: Optional[List[str]] = None, + input_data_query: Optional[Dict[str, Any]] = None, + add_details: bool = False, + ) -> List[InputData]: """ 入力データ一覧を取得する。 """ @@ -176,9 +185,13 @@ def get_input_data_list(self, project_id: str, input_data_id_list: Optional[List return input_data_list - def get_input_data_with_batch(self, project_id: str, batch_query: InputDataBatchQuery, - input_data_query: Optional[Dict[str, Any]] = None, - add_details: bool = False) -> List[InputData]: + def get_input_data_with_batch( + self, + project_id: str, + batch_query: InputDataBatchQuery, + input_data_query: Optional[Dict[str, Any]] = None, + add_details: bool = False, + ) -> List[InputData]: """ バッチ単位で入力データを取得する。 @@ -215,9 +228,14 @@ def get_input_data_with_batch(self, project_id: str, batch_query: InputDataBatch return all_input_data_list - def print_input_data(self, project_id: str, input_data_query: Optional[Dict[str, Any]] = None, - input_data_id_list: Optional[List[str]] = None, add_details: bool = False, - batch_query: Optional[InputDataBatchQuery] = None): + def print_input_data( + self, + project_id: str, + input_data_query: Optional[Dict[str, Any]] = None, + input_data_id_list: Optional[List[str]] = None, + add_details: bool = False, + batch_query: Optional[InputDataBatchQuery] = None, + ): """ 入力データ一覧を出力する @@ -232,18 +250,24 @@ def print_input_data(self, project_id: str, input_data_query: Optional[Dict[str, super().validate_project(project_id, project_member_roles=None) if batch_query is None: - input_data_list = self.get_input_data_list(project_id, input_data_query=input_data_query, - input_data_id_list=input_data_id_list, add_details=add_details) + input_data_list = self.get_input_data_list( + project_id, + input_data_query=input_data_query, + input_data_id_list=input_data_id_list, + add_details=add_details, + ) logger.info(f"入力データ一覧の件数: {len(input_data_list)}") if len(input_data_list) == 10000: logger.warning("入力データ一覧は10,000件で打ち切られている可能性があります。") else: - input_data_list = self.get_input_data_with_batch(project_id, input_data_query=input_data_query, - add_details=add_details, batch_query=batch_query) + input_data_list = self.get_input_data_with_batch( + project_id, input_data_query=input_data_query, add_details=add_details, batch_query=batch_query + ) logger.info(f"入力データ一覧の件数: {len(input_data_list)}") - total_count = self.service.api.get_input_data_list(project_id, - query_params=input_data_query)[0]["total_count"] + total_count = self.service.api.get_input_data_list(project_id, query_params=input_data_query)[0][ + "total_count" + ] if len(input_data_list) != total_count: logger.warning(f"実際に取得した件数:{len(input_data_list)}が、取得可能な件数:{total_count} と異なっていました。") @@ -258,8 +282,13 @@ def main(self): input_data_query = annofabcli.common.cli.get_json_from_args(args.input_data_query) dict_batch_query = annofabcli.common.cli.get_json_from_args(args.batch) batch_query = InputDataBatchQuery.from_dict(dict_batch_query) if dict_batch_query is not None else None - self.print_input_data(args.project_id, input_data_id_list=input_data_id_list, input_data_query=input_data_query, - add_details=args.add_details, batch_query=batch_query) + self.print_input_data( + args.project_id, + input_data_id_list=input_data_id_list, + input_data_query=input_data_query, + add_details=args.add_details, + batch_query=batch_query, + ) def main(args): @@ -276,31 +305,50 @@ def parse_args(parser: argparse.ArgumentParser): query_group = parser.add_mutually_exclusive_group() query_group.add_argument( - '-iq', '--input_data_query', type=str, help='入力データの検索クエリをJSON形式で指定します。' - '`file://`を先頭に付けると、JSON形式のファイルを指定できます。' - 'クエリのフォーマットは、[getInputDataList API](https://annofab.com/docs/api/#operation/getInputDataList)のクエリパラメータと同じです。' - 'ただし `page`, `limit`キーは指定できません。') + "-iq", + "--input_data_query", + type=str, + help="入力データの検索クエリをJSON形式で指定します。" + "`file://`を先頭に付けると、JSON形式のファイルを指定できます。" + "クエリのフォーマットは、[getInputDataList API](https://annofab.com/docs/api/#operation/getInputDataList)のクエリパラメータと同じです。" + "ただし `page`, `limit`キーは指定できません。", + ) query_group.add_argument( - '-i', '--input_data_id', type=str, nargs='+', help='対象のinput_data_idを指定します。`--input_data_query`引数とは同時に指定できません。' - '`file://`を先頭に付けると、input_data_idの一覧が記載されたファイルを指定できます。') + "-i", + "--input_data_id", + type=str, + nargs="+", + help="対象のinput_data_idを指定します。`--input_data_query`引数とは同時に指定できません。" + "`file://`を先頭に付けると、input_data_idの一覧が記載されたファイルを指定できます。", + ) parser.add_argument( - '--batch', type=str, help='段階的に入力データを取得するための情報をJSON形式で指定します。 ' + "--batch", + type=str, + help="段階的に入力データを取得するための情報をJSON形式で指定します。 " '(ex) `{"first":"2019-01-01", "last":"2019-01-31", "days":7}` ' - 'このオプションを駆使すれば、10,000件以上のデータを取得できます。') + "このオプションを駆使すれば、10,000件以上のデータを取得できます。", + ) - parser.add_argument('--add_details', action='store_true', help='入力データの詳細情報を表示します(`parent_task_id_list`)') + parser.add_argument("--add_details", action="store_true", help="入力データの詳細情報を表示します(`parent_task_id_list`)") parser.add_argument( - '--averate_input_data_id_length', type=int, default=36, help=('入力データIDの平均長さを指定します。`add_details`がTrueのときのみ有効です。' - 'デフォルトはUUIDv4の長さです。' - 'この値を元にして、タスク一括取得APIの実行回数を決めます。')) + "--averate_input_data_id_length", + type=int, + default=36, + help=("入力データIDの平均長さを指定します。`add_details`がTrueのときのみ有効です。" "デフォルトはUUIDv4の長さです。" "この値を元にして、タスク一括取得APIの実行回数を決めます。"), + ) argument_parser.add_format( choices=[ - FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON, FormatArgument.INPUT_DATA_ID_LIST - ], default=FormatArgument.CSV) + FormatArgument.CSV, + FormatArgument.JSON, + FormatArgument.PRETTY_JSON, + FormatArgument.INPUT_DATA_ID_LIST, + ], + default=FormatArgument.CSV, + ) argument_parser.add_output() argument_parser.add_csv_format() @@ -311,7 +359,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "list" subcommand_help = "入力データ一覧を出力します。" - description = ("入力データ一覧を出力します。AnnoFabの制約上、10,000件までしか出力されません。") + description = "入力データ一覧を出力します。AnnoFabの制約上、10,000件までしか出力されません。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description) parse_args(parser) diff --git a/annofabcli/input_data/put_input_data.py b/annofabcli/input_data/put_input_data.py index 0b62752b..5c2cac2a 100644 --- a/annofabcli/input_data/put_input_data.py +++ b/annofabcli/input_data/put_input_data.py @@ -15,8 +15,12 @@ import annofabcli from annofabcli import AnnofabApiFacade -from annofabcli.common.cli import (AbstractCommandLineInterface, ArgumentParser, build_annofabapi_resource_and_login, - get_json_from_args) +from annofabcli.common.cli import ( + AbstractCommandLineInterface, + ArgumentParser, + build_annofabapi_resource_and_login, + get_json_from_args, +) from annofabcli.common.dataclasses import WaitOptions from annofabcli.common.utils import get_file_scheme_path @@ -29,6 +33,7 @@ class CsvInputData: """ CSVに記載されている入力データ """ + input_data_name: str input_data_path: str input_data_id: str @@ -39,33 +44,37 @@ class PutInputData(AbstractCommandLineInterface): """ 入力データをCSVで登録する。 """ - def put_input_data(self, project_id: str, csv_input_data: CsvInputData, - last_updated_datetime: Optional[str] = None): - request_body: Dict[str, Any] = {'last_updated_datetime': last_updated_datetime} + def put_input_data( + self, project_id: str, csv_input_data: CsvInputData, last_updated_datetime: Optional[str] = None + ): + + request_body: Dict[str, Any] = {"last_updated_datetime": last_updated_datetime} file_path = get_file_scheme_path(csv_input_data.input_data_path) if file_path is not None: - request_body.update({ - 'input_data_name': csv_input_data.input_data_name, - 'sign_required': csv_input_data.sign_required, - }) + request_body.update( + {"input_data_name": csv_input_data.input_data_name, "sign_required": csv_input_data.sign_required,} + ) logger.debug(f"'{file_path}'を入力データとして登録します。") - self.service.wrapper.put_input_data_from_file(project_id, input_data_id=csv_input_data.input_data_id, - file_path=file_path, request_body=request_body) + self.service.wrapper.put_input_data_from_file( + project_id, input_data_id=csv_input_data.input_data_id, file_path=file_path, request_body=request_body + ) else: - request_body.update({ - 'input_data_name': csv_input_data.input_data_name, - 'input_data_path': csv_input_data.input_data_path, - 'sign_required': csv_input_data.sign_required, - }) + request_body.update( + { + "input_data_name": csv_input_data.input_data_name, + "input_data_path": csv_input_data.input_data_path, + "sign_required": csv_input_data.sign_required, + } + ) self.service.api.put_input_data(project_id, csv_input_data.input_data_id, request_body=request_body) def confirm_put_input_data(self, csv_input_data: CsvInputData, alread_exists: bool = False) -> bool: - message_for_confirm = (f"input_data_name='{csv_input_data.input_data_name}' の入力データを登録しますか?") + message_for_confirm = f"input_data_name='{csv_input_data.input_data_name}' の入力データを登録しますか?" if alread_exists: message_for_confirm += f"input_data_id={csv_input_data.input_data_id} を上書きします。" return self.confirm_processing(message_for_confirm) @@ -101,7 +110,7 @@ def put_input_data_list(self, project_id: str, input_data_list: List[CsvInputDat if input_data is not None: if overwrite: logger.debug(f"input_data_id={input_data_id} はすでに存在します。") - last_updated_datetime = input_data['updated_datetime'] + last_updated_datetime = input_data["updated_datetime"] else: logger.debug(f"input_data_id={input_data_id} がすでに存在するのでスキップします。") continue @@ -119,16 +128,20 @@ def put_input_data_list(self, project_id: str, input_data_list: List[CsvInputDat # 入力データを登録 try: self.put_input_data(project_id, csv_input_data, last_updated_datetime=last_updated_datetime) - logger.debug(f"入力データを登録しました。" - f"input_data_id={csv_input_data.input_data_id}, " - f"input_data_name={csv_input_data.input_data_name}") + logger.debug( + f"入力データを登録しました。" + f"input_data_id={csv_input_data.input_data_id}, " + f"input_data_name={csv_input_data.input_data_name}" + ) count_put_input_data += 1 except requests.exceptions.HTTPError as e: logger.warning(e) - logger.warning(f"入力データの登録に失敗しました。" - f"input_data_id={csv_input_data.input_data_id}, " - f"input_data_name={csv_input_data.input_data_name}") + logger.warning( + f"入力データの登録に失敗しました。" + f"input_data_id={csv_input_data.input_data_id}, " + f"input_data_name={csv_input_data.input_data_name}" + ) logger.info(f"{project_title} に、{count_put_input_data} / {len(input_data_list)} 件の入力データを登録しました。") @@ -137,16 +150,30 @@ def get_input_data_list_from_csv(csv_path: Path) -> List[CsvInputData]: def create_input_data(e): input_data_id = e.input_data_id if not pandas.isna(e.input_data_id) else str(uuid.uuid4()) sign_required = bool(strtobool(str(e.sign_required))) if not pandas.isna(e.sign_required) else None - return CsvInputData(input_data_name=e.input_data_name, input_data_path=e.input_data_path, - input_data_id=input_data_id, sign_required=sign_required) - - df = pandas.read_csv(str(csv_path), sep=',', header=None, - names=('input_data_name', 'input_data_path', 'input_data_id', 'sign_required')) + return CsvInputData( + input_data_name=e.input_data_name, + input_data_path=e.input_data_path, + input_data_id=input_data_id, + sign_required=sign_required, + ) + + df = pandas.read_csv( + str(csv_path), + sep=",", + header=None, + names=("input_data_name", "input_data_path", "input_data_id", "sign_required"), + ) input_data_list = [create_input_data(e) for e in df.itertuples()] return input_data_list - def put_input_data_from_zip_file(self, project_id: str, zip_file: Path, wait_options: WaitOptions, - input_data_name_for_zip: Optional[str] = None, wait: bool = False) -> None: + def put_input_data_from_zip_file( + self, + project_id: str, + zip_file: Path, + wait_options: WaitOptions, + input_data_name_for_zip: Optional[str] = None, + wait: bool = False, + ) -> None: """ zipファイルを入力データとして登録する @@ -165,18 +192,25 @@ def put_input_data_from_zip_file(self, project_id: str, zip_file: Path, wait_opt if input_data_name_for_zip is not None: request_body["input_data_name"] = input_data_name_for_zip - self.service.wrapper.put_input_data_from_file(project_id, input_data_id=str(uuid.uuid4()), - file_path=str(zip_file), content_type="application/zip", - request_body=request_body) + self.service.wrapper.put_input_data_from_file( + project_id, + input_data_id=str(uuid.uuid4()), + file_path=str(zip_file), + content_type="application/zip", + request_body=request_body, + ) logger.info(f"入力データの登録中です(サーバ側の処理)。") if wait: MAX_WAIT_MINUTUE = wait_options.max_tries * wait_options.interval / 60 logger.info(f"最大{MAX_WAIT_MINUTUE}分間、コピーが完了するまで待ちます。") - result = self.service.wrapper.wait_for_completion(project_id, job_type=JobType.GEN_INPUTS, - job_access_interval=wait_options.interval, - max_job_access=wait_options.max_tries) + result = self.service.wrapper.wait_for_completion( + project_id, + job_type=JobType.GEN_INPUTS, + job_access_interval=wait_options.interval, + max_job_access=wait_options.max_tries, + ) if result: logger.info(f"入力データの登録が完了しました。") else: @@ -232,9 +266,13 @@ def main(self): elif args.zip is not None: wait_options = self.get_wait_options_from_args(args) - self.put_input_data_from_zip_file(project_id, zip_file=Path(args.zip), - input_data_name_for_zip=args.input_data_name_for_zip, wait=args.wait, - wait_options=wait_options) + self.put_input_data_from_zip_file( + project_id, + zip_file=Path(args.zip), + input_data_name_for_zip=args.input_data_name_for_zip, + wait=args.wait, + wait_options=wait_options, + ) else: print(f"引数が不正です。", file=sys.stderr) @@ -253,33 +291,43 @@ def parse_args(parser: argparse.ArgumentParser): file_group = parser.add_mutually_exclusive_group(required=True) file_group.add_argument( - '--csv', type=str, - help=('入力データが記載されたCVファイルのパスを指定してください。' - 'CSVのフォーマットは、「1列目:input_data_name(required), 2列目:input_data_path(required), 3列目:input_data_id, ' - '4列目:sign_required(bool), ヘッダ行なし, カンマ区切り」です。' - 'input_data_pathの先頭が`file://`の場合、ローカルのファイルを入力データとして登録します。 ' - 'input_data_idが空の場合はUUIDv4になります。' - '各項目の詳細は `putInputData` API を参照してください。')) + "--csv", + type=str, + help=( + "入力データが記載されたCVファイルのパスを指定してください。" + "CSVのフォーマットは、「1列目:input_data_name(required), 2列目:input_data_path(required), 3列目:input_data_id, " + "4列目:sign_required(bool), ヘッダ行なし, カンマ区切り」です。" + "input_data_pathの先頭が`file://`の場合、ローカルのファイルを入力データとして登録します。 " + "input_data_idが空の場合はUUIDv4になります。" + "各項目の詳細は `putInputData` API を参照してください。" + ), + ) + + file_group.add_argument("--zip", type=str, help=("入力データとして登録するzipファイルのパスを指定してください。")) - file_group.add_argument('--zip', type=str, help=('入力データとして登録するzipファイルのパスを指定してください。')) - - parser.add_argument('--overwrite', action='store_true', - help='指定した場合、input_data_idがすでに存在していたら上書きします。指定しなければ、スキップします。' - '`--csv`を指定したときのみ有効なオプションです。') + parser.add_argument( + "--overwrite", + action="store_true", + help="指定した場合、input_data_idがすでに存在していたら上書きします。指定しなければ、スキップします。" "`--csv`を指定したときのみ有効なオプションです。", + ) parser.add_argument( - '--input_data_name_for_zip', type=str, - help='入力データとして登録するzipファイルのinput_data_nameを指定してください。省略した場合、`--zip`のパスになります。' - '`--zip`を指定したときのみ有効なオプションです。') + "--input_data_name_for_zip", + type=str, + help="入力データとして登録するzipファイルのinput_data_nameを指定してください。省略した場合、`--zip`のパスになります。" "`--zip`を指定したときのみ有効なオプションです。", + ) - parser.add_argument('--wait', action='store_true', help=("入力データの登録が完了するまで待ちます。" "`--zip`を指定したときのみ有効なオプションです。")) + parser.add_argument("--wait", action="store_true", help=("入力データの登録が完了するまで待ちます。" "`--zip`を指定したときのみ有効なオプションです。")) parser.add_argument( - '--wait_options', type=str, help='入力データの登録が完了するまで待つ際のオプションをJSON形式で指定してください。' - '`file://`を先頭に付けるとjsonファイルを指定できます。' + "--wait_options", + type=str, + help="入力データの登録が完了するまで待つ際のオプションをJSON形式で指定してください。" + "`file://`を先頭に付けるとjsonファイルを指定できます。" 'デフォルとは`{"interval":60, "max_tries":360}` です。' - '`interval`:完了したかを問い合わせる間隔[秒], ' - '`max_tires`:完了したかの問い合わせを最大何回行うか。') + "`interval`:完了したかを問い合わせる間隔[秒], " + "`max_tires`:完了したかの問い合わせを最大何回行うか。", + ) parser.set_defaults(subcommand_func=main) @@ -287,7 +335,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "put" subcommand_help = "入力データを登録します。" - description = ("CSVに記載された入力データ情報やzipファイルを、入力データとして登録します。") + description = "CSVに記載された入力データ情報やzipファイルを、入力データとして登録します。" epilog = "オーナロールを持つユーザで実行してください。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog) diff --git a/annofabcli/input_data/subcommand_input_data.py b/annofabcli/input_data/subcommand_input_data.py index c485d91d..99c3312a 100644 --- a/annofabcli/input_data/subcommand_input_data.py +++ b/annofabcli/input_data/subcommand_input_data.py @@ -9,7 +9,7 @@ def parse_args(parser: argparse.ArgumentParser): - subparsers = parser.add_subparsers(dest='subcommand_name') + subparsers = parser.add_subparsers(dest="subcommand_name") # サブコマンドの定義 annofabcli.input_data.delete_input_data.add_parser(subparsers) diff --git a/annofabcli/inspection_comment/list_inspections.py b/annofabcli/inspection_comment/list_inspections.py index 611e1291..2c1aa351 100644 --- a/annofabcli/inspection_comment/list_inspections.py +++ b/annofabcli/inspection_comment/list_inspections.py @@ -28,15 +28,16 @@ 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, + self, + inspection_list: List[Inspection], + task_id_list: Optional[List[str]] = None, + arg_filter_inspection: Optional[FilterInspectionFunc] = None, ) -> List[Inspection]: """ 引数の検査コメント一覧に`commenter_username`など、ユーザが知りたい情報を追加する。 @@ -48,10 +49,11 @@ def filter_inspection_list( 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 + return e["task_id"] in task_id_list def filter_inspection(e): if arg_filter_inspection is None: @@ -61,9 +63,13 @@ def 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, - inspection_list_from_json: Optional[List[Inspection]] = None): + def print_inspections( + self, + project_id: str, + task_id_list: List[str], + filter_inspection: Optional[FilterInspectionFunc] = None, + inspection_list_from_json: Optional[List[Inspection]] = None, + ): """ 検査コメントを出力する @@ -79,12 +85,14 @@ def print_inspections(self, project_id: str, task_id_list: List[str], """ if inspection_list_from_json is None: - inspection_list = self.get_inspections(project_id, task_id_list=task_id_list, - filter_inspection=filter_inspection) + inspection_list = self.get_inspections( + project_id, task_id_list=task_id_list, filter_inspection=filter_inspection + ) else: - inspection_list = self.filter_inspection_list(inspection_list_from_json, task_id_list=task_id_list, - arg_filter_inspection=filter_inspection) + inspection_list = self.filter_inspection_list( + inspection_list_from_json, task_id_list=task_id_list, arg_filter_inspection=filter_inspection + ) logger.info(f"検査コメントの件数: {len(inspection_list)}") @@ -107,8 +115,9 @@ def get_inspections_by_input_data(self, project_id: str, task_id: str, input_dat 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]: + def get_inspections( + self, project_id: str, task_id_list: List[str], filter_inspection: Optional[FilterInspectionFunc] = None + ) -> List[Inspection]: """検査コメント一覧を取得する。 Args: @@ -127,8 +136,9 @@ def get_inspections(self, project_id: str, task_id_list: List[str], 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) + 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)] @@ -146,7 +156,9 @@ def validate(args: argparse.Namespace): if args.inspection_comment_json is None and args.task_id is None: print( "annofabcli inspection_comment list: error: argument -t/--task_id: " - "`--inspection_comment_json`を指定しないときは、必須です。", file=sys.stderr) + "`--inspection_comment_json`を指定しないときは、必須です。", + file=sys.stderr, + ) return False else: return True @@ -171,20 +183,29 @@ def parse_args(parser: argparse.ArgumentParser): argument_parser.add_project_id() argument_parser.add_task_id( - required=False, help_message='対象のタスクのtask_idを指定します。 ' - '`--inspection_comment_json`を指定しないときは、必須です。' - '`file://`を先頭に付けると、task_idの一覧が記載されたファイルを指定できます。') + required=False, + help_message="対象のタスクのtask_idを指定します。 " + "`--inspection_comment_json`を指定しないときは、必須です。" + "`file://`を先頭に付けると、task_idの一覧が記載されたファイルを指定できます。", + ) parser.add_argument( - '--inspection_comment_json', type=str, - help='検査コメント情報が記載されたJSONファイルのパスを指定すると、JSONに記載された情報を元に検査コメント一覧を出力します。AnnoFabから検査コメント情報を取得しません。' - 'JSONには記載されていない、`commenter_username `や`phrase_names_ja`などの情報も追加します。' - 'JSONファイルは`$ annofabcli project download inspection_comment`コマンドで取得できます。') + "--inspection_comment_json", + type=str, + help="検査コメント情報が記載されたJSONファイルのパスを指定すると、JSONに記載された情報を元に検査コメント一覧を出力します。AnnoFabから検査コメント情報を取得しません。" + "JSONには記載されていない、`commenter_username `や`phrase_names_ja`などの情報も追加します。" + "JSONファイルは`$ annofabcli project download inspection_comment`コマンドで取得できます。", + ) argument_parser.add_format( choices=[ - FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON, FormatArgument.INSPECTION_ID_LIST - ], default=FormatArgument.CSV) + FormatArgument.CSV, + FormatArgument.JSON, + FormatArgument.PRETTY_JSON, + FormatArgument.INSPECTION_ID_LIST, + ], + default=FormatArgument.CSV, + ) argument_parser.add_output() argument_parser.add_csv_format() argument_parser.add_query() diff --git a/annofabcli/inspection_comment/list_unprocessed_inspections.py b/annofabcli/inspection_comment/list_unprocessed_inspections.py index baa241dc..c7d094a6 100644 --- a/annofabcli/inspection_comment/list_unprocessed_inspections.py +++ b/annofabcli/inspection_comment/list_unprocessed_inspections.py @@ -43,24 +43,33 @@ def parse_args(parser: argparse.ArgumentParser): argument_parser.add_project_id() argument_parser.add_task_id( - required=False, help_message='対象のタスクのtask_idを指定します。 ' - '`--inspection_comment_json`を指定しないときは、必須です。' - '`file://`を先頭に付けると、task_idの一覧が記載されたファイルを指定できます。') + required=False, + help_message="対象のタスクのtask_idを指定します。 " + "`--inspection_comment_json`を指定しないときは、必須です。" + "`file://`を先頭に付けると、task_idの一覧が記載されたファイルを指定できます。", + ) - parser.add_argument('--inspection_comment', type=str, help='絞り込み条件となる、検査コメントの中身。指定しない場合は絞り込まない。') + parser.add_argument("--inspection_comment", type=str, help="絞り込み条件となる、検査コメントの中身。指定しない場合は絞り込まない。") - parser.add_argument('--commenter_user_id', type=str, help='絞り込み条件となる、検査コメントを付与したユーザのuser_id。 指定しない場合は絞り込まない。') + parser.add_argument("--commenter_user_id", type=str, help="絞り込み条件となる、検査コメントを付与したユーザのuser_id。 指定しない場合は絞り込まない。") parser.add_argument( - '--inspection_comment_json', type=str, - help='検査コメント情報が記載されたJSONファイルのパスを指定すると、JSONに記載された情報を元に検査コメント一覧を出力します。AnnoFabから検査コメント情報を取得しません。' - 'JSONには記載されていない、`commenter_username `や`phrase_names_ja`などの情報も追加します。' - 'JSONファイルは`$ annofabcli project download inspection_comment`コマンドで取得できます。') + "--inspection_comment_json", + type=str, + help="検査コメント情報が記載されたJSONファイルのパスを指定すると、JSONに記載された情報を元に検査コメント一覧を出力します。AnnoFabから検査コメント情報を取得しません。" + "JSONには記載されていない、`commenter_username `や`phrase_names_ja`などの情報も追加します。" + "JSONファイルは`$ annofabcli project download inspection_comment`コマンドで取得できます。", + ) argument_parser.add_format( choices=[ - FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON, FormatArgument.INSPECTION_ID_LIST - ], default=FormatArgument.CSV) + FormatArgument.CSV, + FormatArgument.JSON, + FormatArgument.PRETTY_JSON, + FormatArgument.INSPECTION_ID_LIST, + ], + default=FormatArgument.CSV, + ) argument_parser.add_output() argument_parser.add_csv_format() argument_parser.add_query() @@ -72,7 +81,9 @@ def validate(args: argparse.Namespace): if args.inspection_comment_json is None and args.task_id is None: print( "annofabcli inspection_comment list_unprocessed: error: argument -t/--task_id: " - "`--inspection_comment_json`を指定しないときは、必須です。", file=sys.stderr) + "`--inspection_comment_json`を指定しないときは、必須です。", + file=sys.stderr, + ) return False else: return True @@ -95,9 +106,12 @@ def main(args: argparse.Namespace): else: inspection_list = None - PrintInspections(service, facade, args).print_inspections(project_id=args.project_id, task_id_list=task_id_list, - filter_inspection=filter_inspection, - inspection_list_from_json=inspection_list) + PrintInspections(service, facade, args).print_inspections( + project_id=args.project_id, + task_id_list=task_id_list, + filter_inspection=filter_inspection, + inspection_list_from_json=inspection_list, + ) def add_parser(subparsers: argparse._SubParsersAction): @@ -105,7 +119,7 @@ def add_parser(subparsers: argparse._SubParsersAction): subcommand_help = "未処置の検査コメント一覧を出力する。`task complete` コマンドに渡すデータを取得するのに利用する。" - description = ("未処置の検査コメント一覧を出力する。`task complete` コマンドに渡すデータを取得するのに利用する。") + description = "未処置の検査コメント一覧を出力する。`task complete` コマンドに渡すデータを取得するのに利用する。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description) parse_args(parser) diff --git a/annofabcli/inspection_comment/subcommand_inspection_comment.py b/annofabcli/inspection_comment/subcommand_inspection_comment.py index cd70da9e..042299b6 100644 --- a/annofabcli/inspection_comment/subcommand_inspection_comment.py +++ b/annofabcli/inspection_comment/subcommand_inspection_comment.py @@ -8,7 +8,7 @@ def parse_args(parser: argparse.ArgumentParser): - subparsers = parser.add_subparsers(dest='subcommand_name') + subparsers = parser.add_subparsers(dest="subcommand_name") # サブコマンドの定義 annofabcli.inspection_comment.list_inspections.add_parser(subparsers) diff --git a/annofabcli/instruction/subcommand_instruction.py b/annofabcli/instruction/subcommand_instruction.py index 0263ac7f..3935d7b2 100644 --- a/annofabcli/instruction/subcommand_instruction.py +++ b/annofabcli/instruction/subcommand_instruction.py @@ -7,7 +7,7 @@ def parse_args(parser: argparse.ArgumentParser): - subparsers = parser.add_subparsers(dest='subcommand_name') + subparsers = parser.add_subparsers(dest="subcommand_name") # サブコマンドの定義 annofabcli.instruction.upload_instruction.add_parser(subparsers) diff --git a/annofabcli/instruction/upload_instruction.py b/annofabcli/instruction/upload_instruction.py index 6fd54290..4fecdbb2 100644 --- a/annofabcli/instruction/upload_instruction.py +++ b/annofabcli/instruction/upload_instruction.py @@ -18,20 +18,21 @@ class UploadInstruction(AbstractCommandLineInterface): """ 作業ガイドをアップロードする """ + def upload_html_to_instruction(self, project_id: str, html_path: Path): pq_html = pyquery.PyQuery(filename=str(html_path)) - pq_img = pq_html('img') + pq_img = pq_html("img") # 画像をすべてアップロードして、img要素のsrc属性値を annofab urlに変更する for img_elm in pq_img: - src_value: str = img_elm.attrib.get('src') + src_value: str = img_elm.attrib.get("src") if src_value is None: continue - if src_value.startswith('http:') or src_value.startswith('https:') or src_value.startswith('data:'): + if src_value.startswith("http:") or src_value.startswith("https:") or src_value.startswith("data:"): continue - if src_value[0] == '/': + if src_value[0] == "/": img_path = Path(src_value) else: img_path = html_path.parent / src_value @@ -41,21 +42,21 @@ def upload_html_to_instruction(self, project_id: str, html_path: Path): img_url = self.service.wrapper.upload_instruction_image(project_id, image_id, str(img_path)) logger.debug(f"image uploaded. file={img_path}, instruction_image_url={img_url}") - img_elm.attrib['src'] = img_url + img_elm.attrib["src"] = img_url time.sleep(1) else: logger.warning(f"image does not exist. path={img_path}") # 作業ガイドの更新(body element) - html_data = pq_html('body').html() + html_data = pq_html("body").html() self.update_instruction(project_id, html_data) - logger.info('作業ガイドを更新しました。') + logger.info("作業ガイドを更新しました。") def update_instruction(self, project_id: str, html_data: str): histories, _ = self.service.api.get_instruction_history(project_id) if len(histories) > 0: - last_updated_datetime = histories[0]['updated_datetime'] + last_updated_datetime = histories[0]["updated_datetime"] else: last_updated_datetime = None @@ -80,7 +81,7 @@ def parse_args(parser: argparse.ArgumentParser): argument_parser.add_project_id() # TODO body要素内のみ? - parser.add_argument('--html', type=str, required=True, help='作業ガイドとして登録するHTMLファイルのパスを指定します。') + parser.add_argument("--html", type=str, required=True, help="作業ガイドとして登録するHTMLファイルのパスを指定します。") parser.set_defaults(subcommand_func=main) @@ -88,9 +89,9 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "upload" subcommand_help = "HTMLファイルを作業ガイドとして登録します。" - description = ("HTMLファイルを作業ガイドとして登録します。" - "img要素のsrc属性がローカルの画像を参照している場合(http, https, dataスキーマが付与されていない)、" - "画像もアップロードします。") + description = ( + "HTMLファイルを作業ガイドとして登録します。" "img要素のsrc属性がローカルの画像を参照している場合(http, https, dataスキーマが付与されていない)、" "画像もアップロードします。" + ) epilog = "チェッカーまたはオーナロールを持つユーザで実行してください。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog) parse_args(parser) diff --git a/annofabcli/job/list_job.py b/annofabcli/job/list_job.py index a49371de..a562866a 100644 --- a/annofabcli/job/list_job.py +++ b/annofabcli/job/list_job.py @@ -17,8 +17,10 @@ class ListJob(AbstractCommandLineInterface): """ ジョブ一覧を表示する。 """ - def get_job_list(self, project_id: str, job_type: JobType, job_query: Optional[Dict[str, - Any]] = None) -> List[JobInfo]: + + def get_job_list( + self, project_id: str, job_type: JobType, job_query: Optional[Dict[str, Any]] = None + ) -> List[JobInfo]: """ ジョブ一覧を取得する。 """ @@ -70,7 +72,7 @@ def parse_args(parser: argparse.ArgumentParser): job_choices = [e.value for e in JobType] argument_parser.add_project_id() - parser.add_argument('--job_type', type=str, choices=job_choices, required=True, help='ジョブタイプを指定します。') + parser.add_argument("--job_type", type=str, choices=job_choices, required=True, help="ジョブタイプを指定します。") # クエリがうまく動かないので、コメントアウトする # parser.add_argument( @@ -78,8 +80,9 @@ def parse_args(parser: argparse.ArgumentParser): # '`file://`を先頭に付けると、JSON形式のファイルを指定できます。' # '`limit` キーを指定できます。') - argument_parser.add_format(choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON], - default=FormatArgument.CSV) + argument_parser.add_format( + choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON], default=FormatArgument.CSV + ) argument_parser.add_output() argument_parser.add_csv_format() @@ -90,7 +93,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "list" subcommand_help = "ジョブ一覧を出力します。" - description = ("ジョブ一覧を出力します。") + description = "ジョブ一覧を出力します。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description) parse_args(parser) diff --git a/annofabcli/job/list_last_job.py b/annofabcli/job/list_last_job.py index 0eeb1081..21713a63 100644 --- a/annofabcli/job/list_last_job.py +++ b/annofabcli/job/list_last_job.py @@ -17,6 +17,7 @@ class ListLastJob(AbstractCommandLineInterface): """ ジョブ一覧を表示する。 """ + def get_last_job(self, project_id: str, job_type: JobType) -> Optional[JobInfo]: """ 最新のジョブを取得する。ジョブが存在しない場合はNoneを返す。 @@ -60,8 +61,9 @@ def get_project(self, project_id: str) -> Project: project, _ = self.service.api.get_project(project_id) return project - def get_last_job_list(self, project_id_list: List[str], job_type: JobType, - add_details: bool = False) -> List[JobInfo]: + def get_last_job_list( + self, project_id_list: List[str], job_type: JobType, add_details: bool = False + ) -> List[JobInfo]: job_list = [] for project_id in project_id_list: project = self.get_project(project_id) @@ -104,8 +106,9 @@ def get_project_id_list(self, organization_name: str) -> List[str]: """ my_account, _ = self.service.api.get_my_account() query_params = {"status": "active", "account_id": my_account["account_id"]} - project_list = self.service.wrapper.get_all_projects_of_organization(organization_name, - query_params=query_params) + project_list = self.service.wrapper.get_all_projects_of_organization( + organization_name, query_params=query_params + ) return [e["project_id"] for e in project_list] def main(self): @@ -135,20 +138,28 @@ def parse_args(parser: argparse.ArgumentParser): argument_parser = ArgumentParser(parser) job_choices = [e.value for e in JobType] - parser.add_argument('--job_type', type=str, choices=job_choices, required=True, help='ジョブタイプを指定します。') + parser.add_argument("--job_type", type=str, choices=job_choices, required=True, help="ジョブタイプを指定します。") list_group = parser.add_mutually_exclusive_group(required=True) - list_group.add_argument('-p', '--project_id', type=str, nargs='+', - help='対象のプロジェクトのproject_idを指定してください。`file://`を先頭に付けると、一覧が記載されたファイルを指定できます。') + list_group.add_argument( + "-p", + "--project_id", + type=str, + nargs="+", + help="対象のプロジェクトのproject_idを指定してください。`file://`を先頭に付けると、一覧が記載されたファイルを指定できます。", + ) - list_group.add_argument('-org', '--organization', type=str, help='組織配下のすべてのプロジェクトのジョブを出力したい場合は、組織名を指定してください。') + list_group.add_argument("-org", "--organization", type=str, help="組織配下のすべてのプロジェクトのジョブを出力したい場合は、組織名を指定してください。") parser.add_argument( - '--add_details', action='store_true', help='プロジェクトに関する詳細情報を表示します' - '(`task_last_updated_datetime, annotation_specs_last_updated_datetime`)') - - argument_parser.add_format(choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON], - default=FormatArgument.CSV) + "--add_details", + action="store_true", + help="プロジェクトに関する詳細情報を表示します" "(`task_last_updated_datetime, annotation_specs_last_updated_datetime`)", + ) + + argument_parser.add_format( + choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON], default=FormatArgument.CSV + ) argument_parser.add_output() argument_parser.add_csv_format() @@ -159,7 +170,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "list_last" subcommand_help = "複数のプロジェクトに対して、最新のジョブを出力します。" - description = ("複数のプロジェクトに対して、最新のジョブを出力します。") + description = "複数のプロジェクトに対して、最新のジョブを出力します。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description) parse_args(parser) diff --git a/annofabcli/job/subcommand_job.py b/annofabcli/job/subcommand_job.py index 0f2eb71e..503b18cd 100644 --- a/annofabcli/job/subcommand_job.py +++ b/annofabcli/job/subcommand_job.py @@ -8,7 +8,7 @@ def parse_args(parser: argparse.ArgumentParser): - subparsers = parser.add_subparsers(dest='subcommand_name') + subparsers = parser.add_subparsers(dest="subcommand_name") # サブコマンドの定義 annofabcli.job.list_job.add_parser(subparsers) diff --git a/annofabcli/labor/list_worktime_by_user.py b/annofabcli/labor/list_worktime_by_user.py index 6e0bf61c..88718f42 100644 --- a/annofabcli/labor/list_worktime_by_user.py +++ b/annofabcli/labor/list_worktime_by_user.py @@ -25,6 +25,7 @@ class LaborWorktime: """ 労務管理情報 """ + date: str organization_id: str organization_name: str @@ -43,6 +44,7 @@ class SumLaborWorktime: """ 出力用の作業時間情報 """ + date: str user_id: str worktime_plan_hour: float @@ -64,14 +66,16 @@ def create_required_columns(df, prior_columns): return all_columns @staticmethod - def get_member_from_user_id(organization_member_list: List[OrganizationMember], - user_id: str) -> Optional[OrganizationMember]: + def get_member_from_user_id( + organization_member_list: List[OrganizationMember], user_id: str + ) -> Optional[OrganizationMember]: member = more_itertools.first_true(organization_member_list, pred=lambda e: e["user_id"] == user_id) return member @staticmethod - def get_member_from_account_id(organization_member_list: List[OrganizationMember], - account_id: str) -> Optional[OrganizationMember]: + def get_member_from_account_id( + organization_member_list: List[OrganizationMember], account_id: str + ) -> Optional[OrganizationMember]: member = more_itertools.first_true(organization_member_list, pred=lambda e: e["account_id"] == account_id) return member @@ -94,8 +98,9 @@ def get_worktime_hour(working_time_by_user: Optional[Dict[str, Any]], key: str) else: return value / 3600 / 1000 - def _get_labor_worktime(self, labor: Dict[str, Any], member: Optional[OrganizationMember], project_title: str, - organization_name: str) -> LaborWorktime: + def _get_labor_worktime( + self, labor: Dict[str, Any], member: Optional[OrganizationMember], project_title: str, organization_name: str + ) -> LaborWorktime: new_labor = LaborWorktime( date=labor["date"], organization_id=labor["organization_id"], @@ -110,17 +115,20 @@ def _get_labor_worktime(self, labor: Dict[str, Any], member: Optional[Organizati ) return new_labor - def get_labor_list_from_project_id(self, project_id: str, member_list: List[OrganizationMember], start_date: str, - end_date: str) -> List[LaborWorktime]: + def get_labor_list_from_project_id( + self, project_id: str, member_list: List[OrganizationMember], start_date: str, end_date: str + ) -> List[LaborWorktime]: organization, _ = self.service.api.get_organization_of_project(project_id) organization_name = organization["organization_name"] - labor_list, _ = self.service.api.get_labor_control({ - "project_id": project_id, - "organization_id": organization["organization_id"], - "from": start_date, - "to": end_date - }) + labor_list, _ = self.service.api.get_labor_control( + { + "project_id": project_id, + "organization_id": organization["organization_id"], + "from": start_date, + "to": end_date, + } + ) project_title = self.service.api.get_project(project_id)[0]["title"] logger.info(f"'{project_title}'プロジェクト('{project_id}')の労務管理情報の件数: {len(labor_list)}") @@ -129,37 +137,39 @@ def get_labor_list_from_project_id(self, project_id: str, member_list: List[Orga for labor in labor_list: member = self.get_member_from_account_id(member_list, labor["account_id"]) - new_labor = self._get_labor_worktime(labor, member=member, project_title=project_title, - organization_name=organization_name) + new_labor = self._get_labor_worktime( + labor, member=member, project_title=project_title, organization_name=organization_name + ) new_labor_list.append(new_labor) return new_labor_list - def get_labor_list_from_organization_name(self, organization_name: str, member_list: List[OrganizationMember], - start_date: str, end_date: str) -> List[LaborWorktime]: + def get_labor_list_from_organization_name( + self, organization_name: str, member_list: List[OrganizationMember], start_date: str, end_date: str + ) -> List[LaborWorktime]: organization, _ = self.service.api.get_organization(organization_name) organization_id = organization["organization_id"] project_list = self.service.wrapper.get_all_projects_of_organization(organization_name) - labor_list, _ = self.service.api.get_labor_control({ - "organization_id": organization_id, - "from": start_date, - "to": end_date - }) + labor_list, _ = self.service.api.get_labor_control( + {"organization_id": organization_id, "from": start_date, "to": end_date} + ) logger.info(f"'{organization_name}'組織の労務管理情報の件数: {len(labor_list)}") new_labor_list = [] for labor in labor_list: member = self.get_member_from_account_id(member_list, labor["account_id"]) project_title = self.get_project_title(project_list, labor["project_id"]) - new_labor = self._get_labor_worktime(labor, member=member, project_title=project_title, - organization_name=organization_name) + new_labor = self._get_labor_worktime( + labor, member=member, project_title=project_title, organization_name=organization_name + ) new_labor_list.append(new_labor) return new_labor_list @staticmethod - def get_sum_worktime_list(labor_list: List[LaborWorktime], user_id: str, start_date: str, - end_date: str) -> List[SumLaborWorktime]: + def get_sum_worktime_list( + labor_list: List[LaborWorktime], user_id: str, start_date: str, end_date: str + ) -> List[SumLaborWorktime]: sum_labor_list = [] for date in pandas.date_range(start=start_date, end=end_date): str_date = date.strftime(ListWorktimeByUser.DATE_FORMAT) @@ -167,8 +177,12 @@ def get_sum_worktime_list(labor_list: List[LaborWorktime], user_id: str, start_d worktime_plan_hour = sum([e.worktime_plan_hour for e in filtered_list]) worktime_result_hour = sum([e.worktime_result_hour for e in filtered_list]) - labor = SumLaborWorktime(user_id=user_id, date=date, worktime_plan_hour=worktime_plan_hour, - worktime_result_hour=worktime_result_hour) + labor = SumLaborWorktime( + user_id=user_id, + date=date, + worktime_plan_hour=worktime_plan_hour, + worktime_result_hour=worktime_result_hour, + ) sum_labor_list.append(labor) return sum_labor_list @@ -186,6 +200,7 @@ def write_sum_plan_worktime_list(sum_worktime_df: pandas.DataFrame, output_dir: output_dir: """ + def create_mark(value) -> str: if value == 0: return "×" @@ -209,17 +224,24 @@ def is_plan_column(c) -> bool: @staticmethod def write_worktime_list(worktime_df: pandas.DataFrame, output_dir: Path): - worktime_df = worktime_df.rename(columns={ - "worktime_plan_hour": "作業予定時間", - "worktime_result_hour": "作業実績時間" - }).round(3) + worktime_df = worktime_df.rename( + columns={"worktime_plan_hour": "作業予定時間", "worktime_result_hour": "作業実績時間"} + ).round(3) columns = [ - "date", "organization_name", "project_title", "project_id", "username", "user_id", "作業予定時間", "作業実績時間" + "date", + "organization_name", + "project_title", + "project_id", + "username", + "user_id", + "作業予定時間", + "作業実績時間", ] worktime_df[columns].to_csv(str(output_dir / "作業時間の詳細一覧.csv"), encoding="utf_8_sig", index=False) - def get_organization_member_list(self, organization_name_list: Optional[List[str]], - project_id_list: Optional[List[str]]) -> List[OrganizationMember]: + def get_organization_member_list( + self, organization_name_list: Optional[List[str]], project_id_list: Optional[List[str]] + ) -> List[OrganizationMember]: member_list: List[OrganizationMember] = [] if project_id_list is not None: @@ -236,9 +258,15 @@ def get_organization_member_list(self, organization_name_list: Optional[List[str return member_list - def print_labor_worktime_list(self, organization_name_list: Optional[List[str]], - project_id_list: Optional[List[str]], user_id_list: List[str], start_date: str, - end_date: str, output_dir: Path) -> None: + def print_labor_worktime_list( + self, + organization_name_list: Optional[List[str]], + project_id_list: Optional[List[str]], + user_id_list: List[str], + start_date: str, + end_date: str, + output_dir: Path, + ) -> None: """ 作業時間の一覧を出力する """ @@ -248,14 +276,18 @@ def print_labor_worktime_list(self, organization_name_list: Optional[List[str]], if project_id_list is not None: for project_id in project_id_list: labor_list.extend( - self.get_labor_list_from_project_id(project_id, member_list=member_list, start_date=start_date, - end_date=end_date)) + self.get_labor_list_from_project_id( + project_id, member_list=member_list, start_date=start_date, end_date=end_date + ) + ) elif organization_name_list is not None: for organization_name in organization_name_list: labor_list.extend( - self.get_labor_list_from_organization_name(organization_name, member_list=member_list, - start_date=start_date, end_date=end_date)) + self.get_labor_list_from_organization_name( + organization_name, member_list=member_list, start_date=start_date, end_date=end_date + ) + ) # 集計対象ユーザで絞り込む labor_list = [e for e in labor_list if e.user_id in user_id_list] @@ -268,8 +300,9 @@ def print_labor_worktime_list(self, organization_name_list: Optional[List[str]], } for user_id in user_id_list: - sum_worktime_list = self.get_sum_worktime_list(labor_list, user_id=user_id, start_date=start_date, - end_date=end_date) + sum_worktime_list = self.get_sum_worktime_list( + labor_list, user_id=user_id, start_date=start_date, end_date=end_date + ) member = self.get_member_from_user_id(member_list, user_id) if member is not None: username = member["username"] @@ -277,10 +310,12 @@ def print_labor_worktime_list(self, organization_name_list: Optional[List[str]], logger.warning(f"user_idが'{user_id}'のユーザは存在しません。") username = user_id - reform_dict.update({ - (username, "作業予定"): [e.worktime_plan_hour for e in sum_worktime_list], - (username, "作業実績"): [e.worktime_result_hour for e in sum_worktime_list] - }) + reform_dict.update( + { + (username, "作業予定"): [e.worktime_plan_hour for e in sum_worktime_list], + (username, "作業実績"): [e.worktime_result_hour for e in sum_worktime_list], + } + ) sum_worktime_df = pandas.DataFrame(reform_dict) self.write_sum_worktime_list(sum_worktime_df, output_dir) @@ -305,7 +340,9 @@ def validate(args: argparse.Namespace) -> bool: print( "ERROR: argument --user_id: " "`--start_date/--end_date` または " - "`--start_month/--end_month` の組合わせで指定してください。", file=sys.stderr) + "`--start_month/--end_month` の組合わせで指定してください。", + file=sys.stderr, + ) return False return True @@ -332,8 +369,10 @@ def get_first_and_last_date(str_month: str) -> Tuple[str, str]: dt_first_date = datetime.datetime.strptime(str_month, ListWorktimeByUser.MONTH_FORMAT) _, days = calendar.monthrange(dt_first_date.year, dt_first_date.month) dt_last_date = dt_first_date + datetime.timedelta(days=(days - 1)) - return (dt_first_date.strftime(ListWorktimeByUser.DATE_FORMAT), - dt_last_date.strftime(ListWorktimeByUser.DATE_FORMAT)) + return ( + dt_first_date.strftime(ListWorktimeByUser.DATE_FORMAT), + dt_last_date.strftime(ListWorktimeByUser.DATE_FORMAT), + ) @staticmethod def get_start_and_end_date_from_month(start_month: str, end_month: str) -> Tuple[str, str]: @@ -379,9 +418,14 @@ def main(self) -> None: output_dir = Path(args.output_dir) output_dir.mkdir(exist_ok=True, parents=True) - self.print_labor_worktime_list(organization_name_list=organization_name_list, project_id_list=project_id_list, - start_date=start_date, end_date=end_date, output_dir=output_dir, - user_id_list=user_id_list) # type: ignore + self.print_labor_worktime_list( + organization_name_list=organization_name_list, + project_id_list=project_id_list, + start_date=start_date, + end_date=end_date, + output_dir=output_dir, + user_id_list=user_id_list, + ) # type: ignore def main(args): @@ -392,16 +436,31 @@ def main(args): def parse_args(parser: argparse.ArgumentParser): target_group = parser.add_mutually_exclusive_group(required=True) - target_group.add_argument('-org', '--organization', type=str, nargs='+', - help='集計対象の組織名を指定してください。`file://`を先頭に付けると、組織名の一覧が記載されたファイルを指定できます。') - - target_group.add_argument('-p', '--project_id', type=str, nargs='+', - help='集計対象のプロジェクトを指定してください。`file://`を先頭に付けると、project_idの一覧が記載されたファイルを指定できます。') + target_group.add_argument( + "-org", + "--organization", + type=str, + nargs="+", + help="集計対象の組織名を指定してください。`file://`を先頭に付けると、組織名の一覧が記載されたファイルを指定できます。", + ) + + target_group.add_argument( + "-p", + "--project_id", + type=str, + nargs="+", + help="集計対象のプロジェクトを指定してください。`file://`を先頭に付けると、project_idの一覧が記載されたファイルを指定できます。", + ) parser.add_argument( - '-u', '--user_id', type=str, nargs='+', help='集計対象のユーザのuser_idを指定してください。`--organization`を指定した場合は必須です。' - '指定しない場合は、プロジェクトメンバが指定されます。' - '`file://`を先頭に付けると、user_idの一覧が記載されたファイルを指定できます。') + "-u", + "--user_id", + type=str, + nargs="+", + help="集計対象のユーザのuser_idを指定してください。`--organization`を指定した場合は必須です。" + "指定しない場合は、プロジェクトメンバが指定されます。" + "`file://`を先頭に付けると、user_idの一覧が記載されたファイルを指定できます。", + ) start_period_group = parser.add_mutually_exclusive_group(required=True) start_period_group.add_argument("--start_date", type=str, help="集計期間の開始日(YYYY-MM-DD)") @@ -411,7 +470,7 @@ def parse_args(parser: argparse.ArgumentParser): end_period_group.add_argument("--end_date", type=str, help="集計期間の終了日(YYYY-MM)") end_period_group.add_argument("--end_month", type=str, help="集計期間の終了月(YYYY-MM)") - parser.add_argument('-o', '--output_dir', type=str, required=True, help='出力先のディレクトリのパス') + parser.add_argument("-o", "--output_dir", type=str, required=True, help="出力先のディレクトリのパス") parser.set_defaults(subcommand_func=main) @@ -419,7 +478,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "list_worktime_by_user" subcommand_help = "ユーザごとに作業予定時間、作業実績時間を出力します。" - description = ("ユーザごとに作業予定時間、作業実績時間を出力します。") + description = "ユーザごとに作業予定時間、作業実績時間を出力します。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description) parse_args(parser) diff --git a/annofabcli/labor/subcommand_labor.py b/annofabcli/labor/subcommand_labor.py index b3b60e6a..c5ae9976 100644 --- a/annofabcli/labor/subcommand_labor.py +++ b/annofabcli/labor/subcommand_labor.py @@ -7,7 +7,7 @@ def parse_args(parser: argparse.ArgumentParser): - subparsers = parser.add_subparsers(dest='subcommand_name') + subparsers = parser.add_subparsers(dest="subcommand_name") # サブコマンドの定義 annofabcli.labor.list_worktime_by_user.add_parser(subparsers) diff --git a/annofabcli/organization_member/list_organization_member.py b/annofabcli/organization_member/list_organization_member.py index c4df734c..e75291a0 100644 --- a/annofabcli/organization_member/list_organization_member.py +++ b/annofabcli/organization_member/list_organization_member.py @@ -14,6 +14,7 @@ class ListOrganizationMember(AbstractCommandLineInterface): """ 組織メンバ一覧を表示する。 """ + def print_organization_member_list(self, organization_name: str): """ 組織メンバ一覧を出力する @@ -40,11 +41,12 @@ def main(args): def parse_args(parser: argparse.ArgumentParser): argument_parser = ArgumentParser(parser) - parser.add_argument('-org', '--organization', required=True, type=str, help='対象の組織名を指定してください。') + parser.add_argument("-org", "--organization", required=True, type=str, help="対象の組織名を指定してください。") argument_parser.add_format( choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON, FormatArgument.USER_ID_LIST], - default=FormatArgument.CSV) + default=FormatArgument.CSV, + ) argument_parser.add_output() argument_parser.add_csv_format() argument_parser.add_query() @@ -54,7 +56,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "list" subcommand_help = "組織メンバ一覧を出力します。" - description = ("組織メンバ一覧を出力します。") + description = "組織メンバ一覧を出力します。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description) parse_args(parser) diff --git a/annofabcli/organization_member/subcommand_organization_member.py b/annofabcli/organization_member/subcommand_organization_member.py index c4836466..b711724e 100644 --- a/annofabcli/organization_member/subcommand_organization_member.py +++ b/annofabcli/organization_member/subcommand_organization_member.py @@ -7,7 +7,7 @@ def parse_args(parser: argparse.ArgumentParser): - subparsers = parser.add_subparsers(dest='subcommand_name') + subparsers = parser.add_subparsers(dest="subcommand_name") # サブコマンドの定義 annofabcli.organization_member.list_organization_member.add_parser(subparsers) diff --git a/annofabcli/project/copy_project.py b/annofabcli/project/copy_project.py index 60150d54..25c91ff2 100644 --- a/annofabcli/project/copy_project.py +++ b/annofabcli/project/copy_project.py @@ -8,8 +8,12 @@ import annofabcli from annofabcli import AnnofabApiFacade -from annofabcli.common.cli import (AbstractCommandLineInterface, ArgumentParser, build_annofabapi_resource_and_login, - get_json_from_args) +from annofabcli.common.cli import ( + AbstractCommandLineInterface, + ArgumentParser, + build_annofabapi_resource_and_login, + get_json_from_args, +) from annofabcli.common.dataclasses import WaitOptions logger = logging.getLogger(__name__) @@ -19,9 +23,17 @@ class CopyProject(AbstractCommandLineInterface): """ プロジェクトをコピーする """ - def copy_project(self, src_project_id: str, dest_project_id: str, dest_title: str, wait_options: WaitOptions, - dest_overview: Optional[str] = None, copy_options: Optional[Dict[str, bool]] = None, - wait_for_completion: bool = False): + + def copy_project( + self, + src_project_id: str, + dest_project_id: str, + dest_title: str, + wait_options: WaitOptions, + dest_overview: Optional[str] = None, + copy_options: Optional[Dict[str, bool]] = None, + wait_for_completion: bool = False, + ): """ プロジェクトメンバを、別のプロジェクトにコピーする。 @@ -36,8 +48,10 @@ def copy_project(self, src_project_id: str, dest_project_id: str, dest_title: st """ self.validate_project( - src_project_id, project_member_roles=[ProjectMemberRole.OWNER], - organization_member_roles=[OrganizationMemberRole.ADMINISTRATOR, OrganizationMemberRole.OWNER]) + src_project_id, + project_member_roles=[ProjectMemberRole.OWNER], + organization_member_roles=[OrganizationMemberRole.ADMINISTRATOR, OrganizationMemberRole.OWNER], + ) src_project_title = self.facade.get_project_title(src_project_id) @@ -53,11 +67,9 @@ def copy_project(self, src_project_id: str, dest_project_id: str, dest_title: st if copy_options is not None: request_body = copy.deepcopy(copy_options) - request_body.update({ - "dest_project_id": dest_project_id, - "dest_title": dest_title, - "dest_overview": dest_overview - }) + request_body.update( + {"dest_project_id": dest_project_id, "dest_title": dest_title, "dest_overview": dest_overview} + ) self.service.api.initiate_project_copy(src_project_id, request_body=request_body) logger.info(f"プロジェクトのコピーを実施しています。") @@ -66,9 +78,12 @@ def copy_project(self, src_project_id: str, dest_project_id: str, dest_title: st MAX_WAIT_MINUTUE = wait_options.max_tries * wait_options.interval / 60 logger.info(f"最大{MAX_WAIT_MINUTUE}分間、コピーが完了するまで待ちます。") - result = self.service.wrapper.wait_for_completion(src_project_id, job_type=JobType.COPY_PROJECT, - job_access_interval=wait_options.interval, - max_job_access=wait_options.max_tries) + result = self.service.wrapper.wait_for_completion( + src_project_id, + job_type=JobType.COPY_PROJECT, + job_access_interval=wait_options.interval, + max_job_access=wait_options.max_tries, + ) if result: logger.info(f"プロジェクトのコピーが完了しました。") else: @@ -87,8 +102,12 @@ def main(self): dest_project_id = args.dest_project_id if args.dest_project_id is not None else str(uuid.uuid4()) copy_option_kyes = [ - "copy_inputs", "copy_tasks", "copy_annotations", "copy_webhooks", "copy_supplementaly_data", - "copy_instructions" + "copy_inputs", + "copy_tasks", + "copy_annotations", + "copy_webhooks", + "copy_supplementaly_data", + "copy_instructions", ] copy_options: Dict[str, bool] = {} for key in copy_option_kyes: @@ -96,9 +115,15 @@ def main(self): wait_options = self.get_wait_options_from_args(args) - self.copy_project(args.project_id, dest_project_id=dest_project_id, dest_title=args.dest_title, - dest_overview=args.dest_overview, copy_options=copy_options, wait_for_completion=args.wait, - wait_options=wait_options) + self.copy_project( + args.project_id, + dest_project_id=dest_project_id, + dest_title=args.dest_title, + dest_overview=args.dest_overview, + copy_options=copy_options, + wait_for_completion=args.wait, + wait_options=wait_options, + ) def main(args): @@ -110,27 +135,30 @@ def main(args): def parse_args(parser: argparse.ArgumentParser): argument_parser = ArgumentParser(parser) - argument_parser.add_project_id(help_message='コピー元のプロジェクトのproject_idを指定してください。') + argument_parser.add_project_id(help_message="コピー元のプロジェクトのproject_idを指定してください。") - parser.add_argument('--dest_project_id', type=str, help='新しいプロジェクトのproject_idを指定してください。省略した場合は UUIDv4 フォーマットになります。') - parser.add_argument('--dest_title', type=str, required=True, help="新しいプロジェクトのタイトルを指定してください。") - parser.add_argument('--dest_overview', type=str, help="新しいプロジェクトの概要を指定してください。") + parser.add_argument("--dest_project_id", type=str, help="新しいプロジェクトのproject_idを指定してください。省略した場合は UUIDv4 フォーマットになります。") + parser.add_argument("--dest_title", type=str, required=True, help="新しいプロジェクトのタイトルを指定してください。") + parser.add_argument("--dest_overview", type=str, help="新しいプロジェクトの概要を指定してください。") - parser.add_argument('--copy_inputs', action='store_true', help="「入力データ」をコピーするかどうかを指定します。") - parser.add_argument('--copy_tasks', action='store_true', help="「タスク」をコピーするかどうかを指定します。") - parser.add_argument('--copy_annotations', action='store_true', help="「アノテーション」をコピーするかどうかを指定します。") - parser.add_argument('--copy_webhooks', action='store_true', help="「Webhook」をコピーするかどうかを指定します。") - parser.add_argument('--copy_supplementaly_data', action='store_true', help="「補助情報」をコピーするかどうかを指定します。") - parser.add_argument('--copy_instructions', action='store_true', help="「作業ガイド」をコピーするかどうかを指定します。") + parser.add_argument("--copy_inputs", action="store_true", help="「入力データ」をコピーするかどうかを指定します。") + parser.add_argument("--copy_tasks", action="store_true", help="「タスク」をコピーするかどうかを指定します。") + parser.add_argument("--copy_annotations", action="store_true", help="「アノテーション」をコピーするかどうかを指定します。") + parser.add_argument("--copy_webhooks", action="store_true", help="「Webhook」をコピーするかどうかを指定します。") + parser.add_argument("--copy_supplementaly_data", action="store_true", help="「補助情報」をコピーするかどうかを指定します。") + parser.add_argument("--copy_instructions", action="store_true", help="「作業ガイド」をコピーするかどうかを指定します。") - parser.add_argument('--wait', action='store_true', help="プロジェクトのコピーが完了するまで待ちます。") + parser.add_argument("--wait", action="store_true", help="プロジェクトのコピーが完了するまで待ちます。") parser.add_argument( - '--wait_options', type=str, help='プロジェクトのコピーが完了するまで待つ際のオプションをJSON形式で指定してください。' - '`file://`を先頭に付けるとjsonファイルを指定できます。' + "--wait_options", + type=str, + help="プロジェクトのコピーが完了するまで待つ際のオプションをJSON形式で指定してください。" + "`file://`を先頭に付けるとjsonファイルを指定できます。" 'デフォルとは`{"interval":60, "max_tries":360}` です。' - '`interval`:完了したかを問い合わせる間隔[秒], ' - '`max_tires`:完了したかの問い合わせを最大何回行うか。') + "`interval`:完了したかを問い合わせる間隔[秒], " + "`max_tires`:完了したかの問い合わせを最大何回行うか。", + ) parser.set_defaults(subcommand_func=main) @@ -138,7 +166,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "copy" subcommand_help = "プロジェクトをコピーします。" - description = ("プロジェクトをコピーして(アノテーション仕様やメンバーを引き継いで)、新しいプロジェクトを作成します。") + description = "プロジェクトをコピーして(アノテーション仕様やメンバーを引き継いで)、新しいプロジェクトを作成します。" epilog = "コピー元のプロジェクトに対してオーナロール、組織に対して組織管理者、組織オーナを持つユーザで実行してください。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog) diff --git a/annofabcli/project/diff_projects.py b/annofabcli/project/diff_projects.py index 96f7ca9e..80040b31 100644 --- a/annofabcli/project/diff_projects.py +++ b/annofabcli/project/diff_projects.py @@ -30,6 +30,7 @@ class DiffTarget(Enum): """ 比較する項目 """ + ANNOTATION_LABELS = "annotation_labels" INSPECTION_PHRASES = "inspection_phrases" MEMBERS = "members" @@ -66,6 +67,7 @@ class DiffProjecs(AbstractCommandLineInterface): """ プロジェクト間の差分を表示する """ + def __init__(self, service: annofabapi.Resource, facade: AnnofabApiFacade, args: argparse.Namespace): super().__init__(service, facade, args) @@ -103,9 +105,11 @@ def diff_project_members(self, project_id1: str, project_id2: str) -> DiffResult user_ids2 = [e["user_id"] for e in sorted_members2] if user_ids1 != user_ids2: - diff_message += (f"### user_idのListに差分あり\n" - f"set(user_ids1) - set(user_ids2) = {set(user_ids1) - set(user_ids2)}\n" - f"set(user_ids2) - set(user_ids1) = {set(user_ids2) - set(user_ids1)}\n") + diff_message += ( + f"### user_idのListに差分あり\n" + f"set(user_ids1) - set(user_ids2) = {set(user_ids1) - set(user_ids2)}\n" + f"set(user_ids2) - set(user_ids1) = {set(user_ids2) - set(user_ids1)}\n" + ) return True, diff_message @@ -115,7 +119,7 @@ def diff_project_members(self, project_id1: str, project_id2: str) -> DiffResult diff_result = list(dictdiffer.diff(member1, member2, ignore=ignored_key)) if len(diff_result) > 0: is_different = True - diff_message += (f"差分のあるuser_id: {member1['user_id']}\n" f"{pprint.pformat(diff_result)}\n") + diff_message += f"差分のあるuser_id: {member1['user_id']}\n" f"{pprint.pformat(diff_result)}\n" if not is_different: logger.info("プロジェクトメンバは同じ") @@ -142,17 +146,18 @@ def validate_duplicated(self, label_names1: List[str], label_names2: List[str]) flag = False if len(duplicated_set1) > 0: - diff_message += (f"{self.project_title1}のラベル名(en)が重複しています。{duplicated_set1}\n") + diff_message += f"{self.project_title1}のラベル名(en)が重複しています。{duplicated_set1}\n" flag = True if len(duplicated_set2) > 0: - diff_message += (f"{self.project_title2}のラベル名(en)が重複しています。{duplicated_set2}\n") + diff_message += f"{self.project_title2}のラベル名(en)が重複しています。{duplicated_set2}\n" flag = True return flag, diff_message - def diff_labels_of_annotation_specs(self, labels1: List[Dict[str, Any]], labels2: List[Dict[str, - Any]]) -> DiffResult: + def diff_labels_of_annotation_specs( + self, labels1: List[Dict[str, Any]], labels2: List[Dict[str, Any]] + ) -> DiffResult: """ アノテーションラベル情報の差分を表示する。ラベル名(英語)を基準に差分を表示する。 以下の項目は無視して比較する。 @@ -178,12 +183,12 @@ def diff_labels_of_annotation_specs(self, labels1: List[Dict[str, Any]], labels2 is_duplicated, duplicated_message = self.validate_duplicated(label_names1, label_names2) diff_message += duplicated_message if is_duplicated: - diff_message += (f"ラベル名(en)が重複しているので、アノテーションラベル情報の差分は確認しません。\n") + diff_message += f"ラベル名(en)が重複しているので、アノテーションラベル情報の差分は確認しません。\n" if label_names1 != label_names2: - diff_message += (f"### ラベル名(en)のListに差分あり\n" - f"label_names1: {label_names1}\n" - f"label_names2: {label_names2}\n") + diff_message += ( + f"### ラベル名(en)のListに差分あり\n" f"label_names1: {label_names1}\n" f"label_names2: {label_names2}\n" + ) # 両方に存在するlabel_nameのみ確認する is_different = True @@ -205,7 +210,7 @@ def get_label_func(label_name: str, label: Dict[str, Any]): diff_result = list(dictdiffer.diff(create_ignored_label(label1), create_ignored_label(label2))) if len(diff_result) > 0: is_different = True - diff_message += (f"ラベル名(en): {label_name} は差分あり\n" f"{pprint.pformat(diff_result)}\n") + diff_message += f"ラベル名(en): {label_name} は差分あり\n" f"{pprint.pformat(diff_result)}\n" else: logger.debug(f"ラベル名(en): {label_name} は同じ") @@ -216,8 +221,9 @@ def get_label_func(label_name: str, label: Dict[str, Any]): return is_different, diff_message @staticmethod - def diff_inspection_phrases(inspection_phrases1: List[Dict[str, Any]], - inspection_phrases2: List[Dict[str, Any]]) -> DiffResult: + def diff_inspection_phrases( + inspection_phrases1: List[Dict[str, Any]], inspection_phrases2: List[Dict[str, Any]] + ) -> DiffResult: """ 定型指摘の差分を表示する。定型指摘IDを基準に差分を表示する。 @@ -241,9 +247,11 @@ def diff_inspection_phrases(inspection_phrases1: List[Dict[str, Any]], phrase_ids2 = [e["id"] for e in sorted_inspection_phrases2] if phrase_ids1 != phrase_ids2: - diff_message += (f"### 定型指摘IDのListに差分あり\n" - f"set(phrase_ids1) - set(phrase_ids2) = {set(phrase_ids1) - set(phrase_ids2)}\n" - f"set(phrase_ids2) - set(phrase_ids1) = {set(phrase_ids2) - set(phrase_ids1)}\n") + diff_message += ( + f"### 定型指摘IDのListに差分あり\n" + f"set(phrase_ids1) - set(phrase_ids2) = {set(phrase_ids1) - set(phrase_ids2)}\n" + f"set(phrase_ids2) - set(phrase_ids1) = {set(phrase_ids2) - set(phrase_ids1)}\n" + ) return True, diff_message is_different = False @@ -251,7 +259,7 @@ def diff_inspection_phrases(inspection_phrases1: List[Dict[str, Any]], diff_result = list(dictdiffer.diff(phrase1, phrase2)) if len(diff_result) > 0: is_different = True - diff_message += (f"定型指摘に: {phrase1['id']} は差分あり\n" f"{pprint.pformat(diff_result)}\n") + diff_message += f"定型指摘に: {phrase1['id']} は差分あり\n" f"{pprint.pformat(diff_result)}\n" if not is_different: logger.info("定型指摘は同じ") @@ -278,14 +286,16 @@ def diff_annotation_specs(self, project_id1: str, project_id2: str, diff_targets annotation_specs2, _ = self.service.api.get_annotation_specs(project_id2) if DiffTarget.INSPECTION_PHRASES in diff_targets: - bool_result, message = self.diff_inspection_phrases(annotation_specs1["inspection_phrases"], - annotation_specs2["inspection_phrases"]) + bool_result, message = self.diff_inspection_phrases( + annotation_specs1["inspection_phrases"], annotation_specs2["inspection_phrases"] + ) is_different = is_different or bool_result diff_message += message if DiffTarget.ANNOTATION_LABELS in diff_targets: - bool_result, message = self.diff_labels_of_annotation_specs(annotation_specs1["labels"], - annotation_specs2["labels"]) + bool_result, message = self.diff_labels_of_annotation_specs( + annotation_specs1["labels"], annotation_specs2["labels"] + ) is_different = is_different or bool_result diff_message += message @@ -313,7 +323,7 @@ def diff_project_settingss(self, project_id1: str, project_id2: str) -> DiffResu # ignored_key = {"updated_datetime", "created_datetime", "project_id"} diff_result = list(dictdiffer.diff(config1, config2)) if len(diff_result) > 0: - diff_message += (f"### プロジェクト設定に差分あり\n" f"{pprint.pformat(diff_result)}\n") + diff_message += f"### プロジェクト設定に差分あり\n" f"{pprint.pformat(diff_result)}\n" return True, diff_message else: logger.info("プロジェクト設定は同じ") @@ -359,8 +369,10 @@ def diff(self, project_id1: str, project_id2: str, diff_targets: List[DiffTarget diff_message += message if is_different: - diff_message = (f"!!! {self.project_title1}({project_id1}) と " - f"{self.project_title2}({project_id2}) に差分あり\n" + diff_message) + diff_message = ( + f"!!! {self.project_title1}({project_id1}) と " + f"{self.project_title2}({project_id2}) に差分あり\n" + diff_message + ) return is_different, diff_message def main(self): @@ -374,19 +386,24 @@ def main(self): def parse_args(parser: argparse.ArgumentParser): - parser.add_argument('project_id1', type=str, help='比較対象のプロジェクトのproject_id') + parser.add_argument("project_id1", type=str, help="比較対象のプロジェクトのproject_id") - parser.add_argument('project_id2', type=str, help='比較対象のプロジェクトのproject_id') + parser.add_argument("project_id2", type=str, help="比較対象のプロジェクトのproject_id") choices = [DiffTarget.ANNOTATION_LABELS, DiffTarget.INSPECTION_PHRASES, DiffTarget.MEMBERS, DiffTarget.SETTINGS] parser.add_argument( - '--target', type=str, nargs="+", choices=[e.value for e in choices], - default=["annotation_labels", "inspection_phrases", "members", "settings"], help='比較する項目。指定しなければ全項目を比較する。' - 'annotation_labels: アノテーション仕様のラベル情報, ' - 'inspection_phrases: 定型指摘,' - 'members: プロジェクトメンバ,' - 'settings: プロジェクト設定,') + "--target", + type=str, + nargs="+", + choices=[e.value for e in choices], + default=["annotation_labels", "inspection_phrases", "members", "settings"], + help="比較する項目。指定しなければ全項目を比較する。" + "annotation_labels: アノテーション仕様のラベル情報, " + "inspection_phrases: 定型指摘," + "members: プロジェクトメンバ," + "settings: プロジェクト設定,", + ) parser.set_defaults(subcommand_func=main) @@ -400,7 +417,7 @@ def main(args): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "diff" subcommand_help = "プロジェクト間の差分を表示する。" - description = ("プロジェクト間の差分を表示する。" "ただし、AnnoFabで生成されるIDや、変化する日時などは比較しない。") + description = "プロジェクト間の差分を表示する。" "ただし、AnnoFabで生成されるIDや、変化する日時などは比較しない。" epilog = "オーナ、チェッカーロールのいずれかを持つユーザで実行してください。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog) diff --git a/annofabcli/project/download.py b/annofabcli/project/download.py index 9caf678b..d72e45d4 100644 --- a/annofabcli/project/download.py +++ b/annofabcli/project/download.py @@ -44,9 +44,12 @@ def download(self, target: DownloadTarget, project_id: str, output: str, latest: else: self.service.api.post_project_tasks_update(project_id) - result = self.service.wrapper.wait_for_completion(project_id, job_type=job_type, - job_access_interval=wait_options.interval, - max_job_access=wait_options.max_tries) + result = self.service.wrapper.wait_for_completion( + project_id, + job_type=job_type, + job_access_interval=wait_options.interval, + max_job_access=wait_options.max_tries, + ) if result: logger.info(f"タスクファイルの更新が完了しました。") else: @@ -69,9 +72,12 @@ def download(self, target: DownloadTarget, project_id: str, output: str, latest: else: self.service.api.post_annotation_archive_update(project_id) - result = self.service.wrapper.wait_for_completion(project_id, job_type=job_type, - job_access_interval=wait_options.interval, - max_job_access=wait_options.max_tries) + result = self.service.wrapper.wait_for_completion( + project_id, + job_type=job_type, + job_access_interval=wait_options.interval, + max_job_access=wait_options.max_tries, + ) if result: logger.info(f"アノテーションの更新が完了しました。") else: @@ -91,7 +97,9 @@ def validate(args: argparse.Namespace): download_target = DownloadTarget(args.target) if args.latest: if download_target not in [ - DownloadTarget.TASK, DownloadTarget.SIMPLE_ANNOTATION, DownloadTarget.FULL_ANNOTATION + DownloadTarget.TASK, + DownloadTarget.SIMPLE_ANNOTATION, + DownloadTarget.FULL_ANNOTATION, ]: logger.warning(f"ダウンロード対象が`task`, `simple_annotation`, `full_annotation`以外のときは、`--latest`オプションは無視されます。") @@ -111,8 +119,13 @@ def main(self): return wait_options = self.get_wait_options_from_args(args) - self.download(DownloadTarget(args.target), args.project_id, output=args.output, latest=args.latest, - wait_options=wait_options) + self.download( + DownloadTarget(args.target), + args.project_id, + output=args.output, + latest=args.latest, + wait_options=wait_options, + ) def main(args: argparse.Namespace): @@ -124,22 +137,28 @@ def main(args: argparse.Namespace): def parse_args(parser: argparse.ArgumentParser): target_choices = [e.value for e in DownloadTarget] - parser.add_argument('target', type=str, choices=target_choices, help='ダウンロード対象の項目を指定します。') + parser.add_argument("target", type=str, choices=target_choices, help="ダウンロード対象の項目を指定します。") - parser.add_argument('-p', '--project_id', type=str, required=True, help='対象のプロジェクトのproject_idを指定します。') + parser.add_argument("-p", "--project_id", type=str, required=True, help="対象のプロジェクトのproject_idを指定します。") - parser.add_argument('-o', '--output', type=str, required=True, help='ダウンロード先を指定します。') + parser.add_argument("-o", "--output", type=str, required=True, help="ダウンロード先を指定します。") parser.add_argument( - '--latest', action='store_true', help='ダウンロード対象を最新化してから、ダウンロードします。アノテーションの最新化は5分以上かかる場合があります。' - 'ダウンロード対象が`task`, `simple_annotation`, `full_annotation`のときのみ、このオプションは有効です。') + "--latest", + action="store_true", + help="ダウンロード対象を最新化してから、ダウンロードします。アノテーションの最新化は5分以上かかる場合があります。" + "ダウンロード対象が`task`, `simple_annotation`, `full_annotation`のときのみ、このオプションは有効です。", + ) parser.add_argument( - '--wait_options', type=str, help='ダウンロード対象の最新化を待つときのオプションをJSON形式で指定してください。' - '`file://`を先頭に付けるとjsonファイルを指定できます。' + "--wait_options", + type=str, + help="ダウンロード対象の最新化を待つときのオプションをJSON形式で指定してください。" + "`file://`を先頭に付けるとjsonファイルを指定できます。" 'デフォルとは`{"interval":60, "max_tries":360}` です。' - '`interval`:最新化が完了したかを問い合わせる間隔[秒], ' - '`max_tires`:最新化が完了したかの問い合わせを最大何回行うか。') + "`interval`:最新化が完了したかを問い合わせる間隔[秒], " + "`max_tires`:最新化が完了したかの問い合わせを最大何回行うか。", + ) parser.set_defaults(subcommand_func=main) @@ -147,9 +166,9 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "download" subcommand_help = "タスクや検査コメント、アノテーションなどをダウンロードします。" - description = ("タスクや検査コメント、アノテーションなどをダウンロードします。" - "タスク、検査コメント、タスク履歴イベントは毎日AM 02:00 JSTに更新されます。" - "アノテーションは毎日AM 03:00 JSTに更新されます。") + description = ( + "タスクや検査コメント、アノテーションなどをダウンロードします。" "タスク、検査コメント、タスク履歴イベントは毎日AM 02:00 JSTに更新されます。" "アノテーションは毎日AM 03:00 JSTに更新されます。" + ) epilog = "オーナロールを持つユーザで実行してください。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog) diff --git a/annofabcli/project/list_project.py b/annofabcli/project/list_project.py index 76dc9b99..e78a0f2f 100644 --- a/annofabcli/project/list_project.py +++ b/annofabcli/project/list_project.py @@ -17,6 +17,7 @@ class ListProject(AbstractCommandLineInterface): """ プロジェクト一覧を表示する。 """ + @staticmethod def get_account_id_from_user_id(organization_member_list: List[OrganizationMember], user_id: str) -> Optional[str]: member = first_true(organization_member_list, pred=lambda e: e["user_id"] == user_id) @@ -40,29 +41,30 @@ def _modify_project_query(self, organization_name: str, project_query: Dict[str, 修正したタスク検索クエリ """ + def remove_key(arg_key: str): if arg_key in project_query: logger.info(f"project_query から、`{arg_key}` キーを削除しました。") project_query.pop(arg_key) - remove_key('page') - remove_key('limit') + remove_key("page") + remove_key("limit") organization_member_list = self.service.wrapper.get_all_organization_members(organization_name) - if 'user_id' in project_query: - user_id = project_query['user_id'] + if "user_id" in project_query: + user_id = project_query["user_id"] account_id = self.get_account_id_from_user_id(organization_member_list, user_id) if account_id is not None: - project_query['account_id'] = account_id + project_query["account_id"] = account_id else: logger.warning(f"project_query に含まれている user_id: {user_id} のユーザが見つかりませんでした。") - if 'except_user_id' in project_query: - except_user_id = project_query['except_user_id'] + if "except_user_id" in project_query: + except_user_id = project_query["except_user_id"] except_account_id = self.get_account_id_from_user_id(organization_member_list, except_user_id) if except_account_id is not None: - project_query['except_account_id'] = except_account_id + project_query["except_account_id"] = except_account_id else: logger.warning(f"project_query に含まれている except_user_id: {except_user_id} のユーザが見つかりませんでした。") @@ -77,8 +79,9 @@ def get_project_list(self, organization_name: str, project_query: Optional[Dict[ project_query = self._modify_project_query(organization_name, project_query) logger.debug(f"project_query: {project_query}") - project_list = self.service.wrapper.get_all_projects_of_organization(organization_name, - query_params=project_query) + project_list = self.service.wrapper.get_all_projects_of_organization( + organization_name, query_params=project_query + ) return project_list def print_project_list(self, organization_name: str, project_query: Optional[Dict[str, Any]] = None) -> None: @@ -110,20 +113,24 @@ def main(args): def parse_args(parser: argparse.ArgumentParser): argument_parser = ArgumentParser(parser) - parser.add_argument('-org', '--organization', type=str, required=True, help='対象の組織名を指定してください。') + parser.add_argument("-org", "--organization", type=str, required=True, help="対象の組織名を指定してください。") parser.add_argument( - '--project_query', type=str, help='タスクの検索クエリをJSON形式で指定します。' - '指定しない場合は、組織配下のすべてのプロジェクトを取得します。' - '`file://`を先頭に付けると、JSON形式のファイルを指定できます。' - 'クエリのフォーマットは、[getProjectsOfOrganization API]' - '(https://annofab.com/docs/api/#operation/getProjectsOfOrganization)のクエリパラメータと同じです。' - 'さらに追加で、`user_id`, `except_user_id` キーも指定できます。' - 'ただし `page`, `limit`キーは指定できません。') + "--project_query", + type=str, + help="タスクの検索クエリをJSON形式で指定します。" + "指定しない場合は、組織配下のすべてのプロジェクトを取得します。" + "`file://`を先頭に付けると、JSON形式のファイルを指定できます。" + "クエリのフォーマットは、[getProjectsOfOrganization API]" + "(https://annofab.com/docs/api/#operation/getProjectsOfOrganization)のクエリパラメータと同じです。" + "さらに追加で、`user_id`, `except_user_id` キーも指定できます。" + "ただし `page`, `limit`キーは指定できません。", + ) argument_parser.add_format( choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON, FormatArgument.PROJECT_ID_LIST], - default=FormatArgument.CSV) + default=FormatArgument.CSV, + ) argument_parser.add_output() argument_parser.add_csv_format() @@ -134,7 +141,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "list" subcommand_help = "プロジェクト一覧を出力します。" - description = ("プロジェクト一覧を出力します。") + description = "プロジェクト一覧を出力します。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description) parse_args(parser) diff --git a/annofabcli/project/subcommand_project.py b/annofabcli/project/subcommand_project.py index 25d68c2c..0b9a589b 100644 --- a/annofabcli/project/subcommand_project.py +++ b/annofabcli/project/subcommand_project.py @@ -10,7 +10,7 @@ def parse_args(parser: argparse.ArgumentParser): - subparsers = parser.add_subparsers(dest='subcommand_name') + subparsers = parser.add_subparsers(dest="subcommand_name") # サブコマンドの定義 annofabcli.project.copy_project.add_parser(subparsers) diff --git a/annofabcli/project_member/change_project_members.py b/annofabcli/project_member/change_project_members.py index 70f6e787..4942903b 100644 --- a/annofabcli/project_member/change_project_members.py +++ b/annofabcli/project_member/change_project_members.py @@ -18,9 +18,15 @@ class ChangeProjectMembers(AbstractCommandLineInterface): """ プロジェクトメンバのメンバ情報を更新する。 """ - def put_project_member(self, project_id: str, user_id: str, old_member: ProjectMember, - member_role: Optional[ProjectMemberRole] = None, - member_info: Optional[Dict[str, Any]] = None) -> ProjectMember: + + def put_project_member( + self, + project_id: str, + user_id: str, + old_member: ProjectMember, + member_role: Optional[ProjectMemberRole] = None, + member_info: Optional[Dict[str, Any]] = None, + ) -> ProjectMember: """ 1人のプロジェクトメンバを変更する。 @@ -34,6 +40,7 @@ def put_project_member(self, project_id: str, user_id: str, old_member: ProjectM Returns: """ + def get_value(key): if member_info is None: return old_member[key] @@ -56,8 +63,13 @@ def get_value(key): updated_project_member = self.service.api.put_project_member(project_id, user_id, request_body=request_body)[0] return updated_project_member - def change_project_members(self, project_id: str, user_id_list, member_role: Optional[ProjectMemberRole] = None, - member_info: Optional[Dict[str, Any]] = None): + def change_project_members( + self, + project_id: str, + user_id_list, + member_role: Optional[ProjectMemberRole] = None, + member_info: Optional[Dict[str, Any]] = None, + ): """ プロジェクトメンバのメンバ情報を更新する。 @@ -87,7 +99,7 @@ def change_project_members(self, project_id: str, user_id_list, member_role: Opt logger.warning(f"ユーザ '{user_id}' は、プロジェクトメンバでないため変更できませんでした。") continue - message_for_confirm = (f"ユーザ '{user_id}'のプロジェクトメンバ情報を変更しますか?") + message_for_confirm = f"ユーザ '{user_id}'のプロジェクトメンバ情報を変更しますか?" if not self.confirm_processing(message_for_confirm): continue @@ -95,7 +107,8 @@ def change_project_members(self, project_id: str, user_id_list, member_role: Opt try: self.put_project_member(project_id, user_id, member_info=member_info, old_member=old_member) logger.debug( - f"user_id = {user_id} のプロジェクトメンバ情報を変更しました。member_role={member_role}, member_info={member_info}") + f"user_id = {user_id} のプロジェクトメンバ情報を変更しました。member_role={member_role}, member_info={member_info}" + ) count_invite_members += 1 except requests.exceptions.HTTPError as e: @@ -107,7 +120,7 @@ def change_project_members(self, project_id: str, user_id_list, member_role: Opt def get_all_user_id_list_except_myself(self, project_id: str) -> List[str]: """自分自身を除いた、すべてのプロジェクトメンバを取得する""" member_list = self.service.wrapper.get_all_project_members(project_id) - return [e["user_id"] for e in member_list if e['user_id'] != self.service.api.login_user_id] + return [e["user_id"] for e in member_list if e["user_id"] != self.service.api.login_user_id] @staticmethod def validate(args: argparse.Namespace, member_info: Optional[Dict[str, Any]] = None) -> bool: @@ -142,8 +155,9 @@ def main(self): if not self.validate(args, member_info): return - self.change_project_members(args.project_id, user_id_list=user_id_list, member_role=member_role, - member_info=member_info) + self.change_project_members( + args.project_id, user_id_list=user_id_list, member_role=member_role, member_info=member_info + ) def main(args): @@ -159,19 +173,28 @@ def parse_args(parser: argparse.ArgumentParser): argument_parser.add_project_id() user_group = parser.add_mutually_exclusive_group(required=True) - user_group.add_argument('-u', '--user_id', type=str, nargs='+', help='変更するプロジェクトメンバのuser_idを指定してください。' - '`file://`を先頭に付けると、一覧が記載されたファイルを指定できます。') - user_group.add_argument('--all_user', action='store_true', help='自分以外のすべてのプロジェクトメンバを変更します。') + user_group.add_argument( + "-u", + "--user_id", + type=str, + nargs="+", + help="変更するプロジェクトメンバのuser_idを指定してください。" "`file://`を先頭に付けると、一覧が記載されたファイルを指定できます。", + ) + user_group.add_argument("--all_user", action="store_true", help="自分以外のすべてのプロジェクトメンバを変更します。") - parser.add_argument('--role', type=str, choices=role_choices, - help='プロジェクトメンバにユーザに割り当てるロールを指定します。指定しない場合は、ロールは変更されません。') + parser.add_argument( + "--role", type=str, choices=role_choices, help="プロジェクトメンバにユーザに割り当てるロールを指定します。指定しない場合は、ロールは変更されません。" + ) parser.add_argument( - '--member_info', type=str, required=True, + "--member_info", + type=str, + required=True, help="プロジェクトメンバに対して設定するメンバ情報を、JSON形式で指定します。`file://`を先頭に付けると、JSON形式のファイルを指定できます。 " "以下のキーが指定可能です。sampling_inspection_rate, sampling_acceptance_rate, " "未設定にする場合は、値にnullを指定してください。" - "詳細は https://annofab.com/docs/api/#operation/putProjectMember を参照ください。 ") + "詳細は https://annofab.com/docs/api/#operation/putProjectMember を参照ください。 ", + ) parser.set_defaults(subcommand_func=main) @@ -179,7 +202,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "change" subcommand_help = "プロジェクトメンバを変更します。" - description = ("複数のプロジェクトメンバに対して、メンバ情報を変更します。ただし、自分自身は変更できません。") + description = "複数のプロジェクトメンバに対して、メンバ情報を変更します。ただし、自分自身は変更できません。" epilog = "オーナロールを持つユーザで実行してください。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog) diff --git a/annofabcli/project_member/copy_project_members.py b/annofabcli/project_member/copy_project_members.py index 6dd88c05..2648c90d 100644 --- a/annofabcli/project_member/copy_project_members.py +++ b/annofabcli/project_member/copy_project_members.py @@ -16,6 +16,7 @@ class CopyProjectMembers(AbstractCommandLineInterface): """ プロジェクトメンバをコピーする """ + def __init__(self, service: annofabapi.Resource, facade: AnnofabApiFacade, args: argparse.Namespace): super().__init__(service, facade, args) @@ -81,8 +82,10 @@ def copy_project_members(self, src_project_id: str, dest_project_id: str, delete account_id = member["account_id"] if self.find_member(dest_organization_members, account_id) is None: # コピー先の組織メンバでないので、コピーしない - logger.debug(f"コピーしないメンバ: {member['user_id']} , {member['username']} : " - f"(コピー先の所属組織 {dest_organization_name} の組織メンバでないため)") + logger.debug( + f"コピーしないメンバ: {member['user_id']} , {member['username']} : " + f"(コピー先の所属組織 {dest_organization_name} の組織メンバでないため)" + ) continue added_members.append(member) @@ -100,7 +103,7 @@ def copy_project_members(self, src_project_id: str, dest_project_id: str, delete deleted_dest_members = [e for e in dest_project_members if e["account_id"] not in src_account_ids] def to_inactive(arg_member): - arg_member['member_status'] = 'inactive' + arg_member["member_status"] = "inactive" return arg_member deleted_dest_members = list(map(to_inactive, deleted_dest_members)) @@ -110,9 +113,11 @@ def to_inactive(arg_member): updated_members = updated_members + deleted_dest_members if len(added_members) > 0: - if self.confirm_processing(f"{self.src_project_title} プロジェクトのメンバを、" - f"{self.dest_project_title} にコピーしますか?" - f"追加対象: {len(added_members)} 件, 削除対象: {len(deleted_dest_members)} 件"): + if self.confirm_processing( + f"{self.src_project_title} プロジェクトのメンバを、" + f"{self.dest_project_title} にコピーしますか?" + f"追加対象: {len(added_members)} 件, 削除対象: {len(deleted_dest_members)} 件" + ): self.service.wrapper.put_project_members(dest_project_id, updated_members) else: logger.info(f"{self.dest_project_title}のプロジェクトメンバに追加/更新はありません。") @@ -129,10 +134,10 @@ def main(args): def parse_args(parser: argparse.ArgumentParser): - parser.add_argument('src_project_id', type=str, help='コピー元のプロジェクトのproject_id') - parser.add_argument('dest_project_id', type=str, help='コピー先のプロジェクトのproject_id') + parser.add_argument("src_project_id", type=str, help="コピー元のプロジェクトのproject_id") + parser.add_argument("dest_project_id", type=str, help="コピー先のプロジェクトのproject_id") - parser.add_argument('--delete_dest', action='store_true', help='コピー先のプロジェクトにしか存在しないプロジェクトメンバを削除します。') + parser.add_argument("--delete_dest", action="store_true", help="コピー先のプロジェクトにしか存在しないプロジェクトメンバを削除します。") parser.set_defaults(subcommand_func=main) @@ -140,7 +145,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "copy" subcommand_help = "プロジェクトメンバをコピーする。" - description = ("プロジェクトメンバをコピーする。") + description = "プロジェクトメンバをコピーする。" epilog = "コピー先のプロジェクトに対して、オーナロールを持つユーザで実行してください。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog) diff --git a/annofabcli/project_member/delete_users.py b/annofabcli/project_member/delete_users.py index 9b6b09e8..e57d6681 100644 --- a/annofabcli/project_member/delete_users.py +++ b/annofabcli/project_member/delete_users.py @@ -18,15 +18,14 @@ class DeleteUser(AbstractCommandLineInterface): """ ユーザをプロジェクトから削除する """ + def drop_role_with_organization(self, organization_name: str, user_id_list: List[str]): # 進行中で自分自身が所属しているプロジェクトの一覧を取得する my_account_id = self.facade.get_my_account_id() projects = self.service.wrapper.get_all_projects_of_organization( - organization_name, query_params={ - "status": "active", - "account_id": my_account_id - }) + organization_name, query_params={"status": "active", "account_id": my_account_id} + ) for project in projects: project_id = project["project_id"] @@ -34,8 +33,9 @@ def drop_role_with_organization(self, organization_name: str, user_id_list: List try: if not self.facade.my_role_is_owner(project_id): - logger.warning(f"オーナではないため、プロジェクトメンバを削除できません。" - f"project_id = {project_id}, project_tilte = {project_title}") + logger.warning( + f"オーナではないため、プロジェクトメンバを削除できません。" f"project_id = {project_id}, project_tilte = {project_title}" + ) continue self.service.wrapper.drop_role_to_project_members(project_id, user_id_list) @@ -80,14 +80,25 @@ def main(args): def parse_args(parser: argparse.ArgumentParser): - parser.add_argument('-u', '--user_id', type=str, nargs='+', required=True, - help='削除するユーザのuser_idを指定してください。`file://`を先頭に付けると、一覧が記載されたファイルを指定できます。') + parser.add_argument( + "-u", + "--user_id", + type=str, + nargs="+", + required=True, + help="削除するユーザのuser_idを指定してください。`file://`を先頭に付けると、一覧が記載されたファイルを指定できます。", + ) drop_group = parser.add_mutually_exclusive_group(required=True) - drop_group.add_argument('-p', '--project_id', type=str, nargs='+', - help='削除するプロジェクトのproject_idを指定してください。`file://`を先頭に付けると、一覧が記載されたファイルを指定できます。') + drop_group.add_argument( + "-p", + "--project_id", + type=str, + nargs="+", + help="削除するプロジェクトのproject_idを指定してください。`file://`を先頭に付けると、一覧が記載されたファイルを指定できます。", + ) - drop_group.add_argument('-org', '--organization', type=str, help='組織配下のすべての進行中のプロジェクトから削除したい場合は、組織名を指定してください。') + drop_group.add_argument("-org", "--organization", type=str, help="組織配下のすべての進行中のプロジェクトから削除したい場合は、組織名を指定してください。") parser.set_defaults(subcommand_func=main) @@ -95,7 +106,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "delete" subcommand_help = "複数のプロジェクトから、ユーザを削除する。" - description = ("複数のプロジェクトから、ユーザを削除する。") + description = "複数のプロジェクトから、ユーザを削除する。" epilog = "オーナロールを持つユーザで実行してください。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog) diff --git a/annofabcli/project_member/invite_users.py b/annofabcli/project_member/invite_users.py index a602dda3..aaeb9d52 100644 --- a/annofabcli/project_member/invite_users.py +++ b/annofabcli/project_member/invite_users.py @@ -21,15 +21,14 @@ class InviteUser(AbstractCommandLineInterface): """ ユーザをプロジェクトに招待する """ + def assign_role_with_organization(self, organization_name: str, user_id_list: List[str], member_role: str): # 進行中で自分自身が所属しているプロジェクトの一覧を取得する my_account_id = self.facade.get_my_account_id() projects = self.service.wrapper.get_all_projects_of_organization( - organization_name, query_params={ - "status": "active", - "account_id": my_account_id - }) + organization_name, query_params={"status": "active", "account_id": my_account_id} + ) for project in projects: project_id = project["project_id"] @@ -37,8 +36,9 @@ def assign_role_with_organization(self, organization_name: str, user_id_list: Li try: if not self.facade.my_role_is_owner(project_id): - logger.warning(f"オーナではないため、プロジェクトメンバを招待できません。" - f"project_id = {project_id}, project_tilte = {project_title}") + logger.warning( + f"オーナではないため、プロジェクトメンバを招待できません。" f"project_id = {project_id}, project_tilte = {project_title}" + ) continue self.service.wrapper.assign_role_to_project_members(project_id, user_id_list, member_role) @@ -85,19 +85,32 @@ def main(args): def parse_args(parser: argparse.ArgumentParser): role_choices = [ - ProjectMemberRole.OWNER.value, ProjectMemberRole.WORKER.value, ProjectMemberRole.ACCEPTER.value, - ProjectMemberRole.TRAINING_DATA_USER.value + ProjectMemberRole.OWNER.value, + ProjectMemberRole.WORKER.value, + ProjectMemberRole.ACCEPTER.value, + ProjectMemberRole.TRAINING_DATA_USER.value, ] - parser.add_argument('-u', '--user_id', type=str, nargs='+', required=True, - help='招待するユーザのuser_idを指定してください。`file://`を先頭に付けると、一覧が記載されたファイルを指定できます。') + parser.add_argument( + "-u", + "--user_id", + type=str, + nargs="+", + required=True, + help="招待するユーザのuser_idを指定してください。`file://`を先頭に付けると、一覧が記載されたファイルを指定できます。", + ) - parser.add_argument('--role', type=str, required=True, choices=role_choices, help='ユーザに割り当てるロール') + parser.add_argument("--role", type=str, required=True, choices=role_choices, help="ユーザに割り当てるロール") assign_group = parser.add_mutually_exclusive_group(required=True) - assign_group.add_argument('-p', '--project_id', type=str, nargs='+', - help='招待するプロジェクトのproject_idを指定してください。`file://`を先頭に付けると、一覧が記載されたファイルを指定できます。') - assign_group.add_argument('-org', '--organization', type=str, help='組織配下のすべての進行中のプロジェクトに招待したい場合は、組織名を指定してください。') + assign_group.add_argument( + "-p", + "--project_id", + type=str, + nargs="+", + help="招待するプロジェクトのproject_idを指定してください。`file://`を先頭に付けると、一覧が記載されたファイルを指定できます。", + ) + assign_group.add_argument("-org", "--organization", type=str, help="組織配下のすべての進行中のプロジェクトに招待したい場合は、組織名を指定してください。") parser.set_defaults(subcommand_func=main) @@ -105,7 +118,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "invite" subcommand_help = "複数のプロジェクトに、ユーザを招待する。" - description = ("複数のプロジェクトに、ユーザを招待する。") + description = "複数のプロジェクトに、ユーザを招待する。" epilog = "オーナロールを持つユーザで実行してください。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog) diff --git a/annofabcli/project_member/list_users.py b/annofabcli/project_member/list_users.py index 1fad5ba2..87b77144 100644 --- a/annofabcli/project_member/list_users.py +++ b/annofabcli/project_member/list_users.py @@ -22,35 +22,36 @@ class ListUser(AbstractCommandLineInterface): """ ユーザを表示する """ - def get_all_project_members(self, project_id: str, include_inactive: bool = False, - include_outside_organization: bool = False): + + def get_all_project_members( + self, project_id: str, include_inactive: bool = False, include_outside_organization: bool = False + ): def member_exists(members: List[Dict[str, Any]], account_id) -> bool: - m = more_itertools.first_true(members, default=None, pred=lambda e: e['account_id'] == account_id) + m = more_itertools.first_true(members, default=None, pred=lambda e: e["account_id"] == account_id) return m is not None query_params = {} if include_inactive: - query_params.update({'include_inactive_member': ''}) + query_params.update({"include_inactive_member": ""}) project_members = self.service.wrapper.get_all_project_members(project_id, query_params=query_params) organization_members = self.facade.get_organization_members_from_project_id(project_id) # 組織外のメンバを除外 if not include_outside_organization: - project_members = [e for e in project_members if member_exists(organization_members, e['account_id'])] + project_members = [e for e in project_members if member_exists(organization_members, e["account_id"])] return project_members - def get_project_members_with_organization(self, organization_name: str, include_inactive: bool = False, - include_outside_organization: bool = False) -> List[ProjectMember]: + def get_project_members_with_organization( + self, organization_name: str, include_inactive: bool = False, include_outside_organization: bool = False + ) -> List[ProjectMember]: # 進行中で自分自身が所属しているプロジェクトの一覧を取得する my_account_id = self.facade.get_my_account_id() projects = self.service.wrapper.get_all_projects_of_organization( - organization_name, query_params={ - "status": "active", - "account_id": my_account_id - }) + organization_name, query_params={"status": "active", "account_id": my_account_id} + ) all_project_members: List[ProjectMember] = [] @@ -58,8 +59,9 @@ def get_project_members_with_organization(self, organization_name: str, include_ project_id = project["project_id"] project_title = project["title"] - project_members = self.get_all_project_members(project_id, include_inactive=include_inactive, - include_outside_organization=include_outside_organization) + project_members = self.get_all_project_members( + project_id, include_inactive=include_inactive, include_outside_organization=include_outside_organization + ) logger.info(f"{project_title} のプロジェクトメンバを {len(project_members)} 件取得した。project_id={project_id}") for member in project_members: @@ -69,8 +71,9 @@ def get_project_members_with_organization(self, organization_name: str, include_ return all_project_members - def get_project_members_with_project_id(self, project_id_list: List[str], include_inactive: bool = False, - include_outside_organization: bool = False) -> List[ProjectMember]: + def get_project_members_with_project_id( + self, project_id_list: List[str], include_inactive: bool = False, include_outside_organization: bool = False + ) -> List[ProjectMember]: all_project_members: List[ProjectMember] = [] for project_id in project_id_list: @@ -82,8 +85,9 @@ def get_project_members_with_project_id(self, project_id_list: List[str], includ continue project_title = project["title"] - project_members = self.get_all_project_members(project_id, include_inactive=include_inactive, - include_outside_organization=include_outside_organization) + project_members = self.get_all_project_members( + project_id, include_inactive=include_inactive, include_outside_organization=include_outside_organization + ) logger.info(f"{project_title} のプロジェクトメンバを {len(project_members)} 件取得した。project_id={project_id}") for member in project_members: @@ -99,14 +103,18 @@ def main(self): project_members = [] if args.organization is not None: project_members = self.get_project_members_with_organization( - args.organization, include_inactive=args.include_inactive, - include_outside_organization=args.include_outside_organization) + args.organization, + include_inactive=args.include_inactive, + include_outside_organization=args.include_outside_organization, + ) elif args.project_id is not None: project_id_list = annofabcli.common.cli.get_list_from_args(args.project_id) project_members = self.get_project_members_with_project_id( - project_id_list, include_inactive=args.include_inactive, - include_outside_organization=args.include_outside_organization) + project_id_list, + include_inactive=args.include_inactive, + include_outside_organization=args.include_outside_organization, + ) self.print_according_to_format(project_members) @@ -121,21 +129,30 @@ def parse_args(parser: argparse.ArgumentParser): argument_parser = ArgumentParser(parser) list_group = parser.add_mutually_exclusive_group(required=True) - list_group.add_argument('-p', '--project_id', type=str, nargs='+', - help='ユーザを表示するプロジェクトのproject_idを指定してください。`file://`を先頭に付けると、一覧が記載されたファイルを指定できます。') + list_group.add_argument( + "-p", + "--project_id", + type=str, + nargs="+", + help="ユーザを表示するプロジェクトのproject_idを指定してください。`file://`を先頭に付けると、一覧が記載されたファイルを指定できます。", + ) - list_group.add_argument('-org', '--organization', type=str, - help='組織配下のすべての進行中のプロジェクトのプロジェクトメンバを表示したい場合は、組織名を指定してください。') + list_group.add_argument( + "-org", "--organization", type=str, help="組織配下のすべての進行中のプロジェクトのプロジェクトメンバを表示したい場合は、組織名を指定してください。" + ) - parser.add_argument('--include_inactive', action='store_true', help='脱退されたメンバも表示します。') + parser.add_argument("--include_inactive", action="store_true", help="脱退されたメンバも表示します。") parser.add_argument( - '--include_outside_organization', action='store_true', help=('組織外のメンバも表示します。通常のプロジェクトでは組織外のメンバは表示されません。' - '所属組織を途中で変更したプロジェクトの場合、組織外のメンバが含まれている可能性があります。')) + "--include_outside_organization", + action="store_true", + help=("組織外のメンバも表示します。通常のプロジェクトでは組織外のメンバは表示されません。" "所属組織を途中で変更したプロジェクトの場合、組織外のメンバが含まれている可能性があります。"), + ) argument_parser.add_format( choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON, FormatArgument.USER_ID_LIST], - default=FormatArgument.CSV) + default=FormatArgument.CSV, + ) argument_parser.add_output() argument_parser.add_csv_format() argument_parser.add_query() @@ -146,7 +163,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "list" subcommand_help = "複数のプロジェクトのプロジェクトメンバを表示する。" - description = ("複数のプロジェクトのプロジェクトメンバを表示する。") + description = "複数のプロジェクトのプロジェクトメンバを表示する。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description) parse_args(parser) diff --git a/annofabcli/project_member/put_project_members.py b/annofabcli/project_member/put_project_members.py index c75fefce..f4ed134d 100644 --- a/annofabcli/project_member/put_project_members.py +++ b/annofabcli/project_member/put_project_members.py @@ -23,6 +23,7 @@ class Member: """ 登録するプロジェクトメンバ """ + user_id: str member_role: ProjectMemberRole sampling_inspection_rate: Optional[int] @@ -33,9 +34,10 @@ class PutProjectMembers(AbstractCommandLineInterface): """ プロジェクトメンバをCSVで登録する。 """ + @staticmethod def find_member(members: List[Dict[str, Any]], user_id: str) -> Optional[Dict[str, Any]]: - member = more_itertools.first_true(members, default=None, pred=lambda e: e['user_id'] == user_id) + member = more_itertools.first_true(members, default=None, pred=lambda e: e["user_id"] == user_id) return member @staticmethod @@ -53,18 +55,20 @@ def invite_project_member(self, project_id, member: Member, old_project_members: "sampling_acceptance_rate": member.sampling_acceptance_rate, "last_updated_datetime": last_updated_datetime, } - updated_project_member = self.service.api.put_project_member(project_id, member.user_id, - request_body=request_body)[0] + updated_project_member = self.service.api.put_project_member( + project_id, member.user_id, request_body=request_body + )[0] return updated_project_member def delete_project_member(self, project_id, deleted_member: Dict[str, Any]): request_body = { "member_status": ProjectMemberStatus.INACTIVE.value, - "member_role": deleted_member['member_role'], - "last_updated_datetime": deleted_member['updated_datetime'], + "member_role": deleted_member["member_role"], + "last_updated_datetime": deleted_member["updated_datetime"], } - updated_project_member = self.service.api.put_project_member(project_id, deleted_member['user_id'], - request_body=request_body)[0] + updated_project_member = self.service.api.put_project_member( + project_id, deleted_member["user_id"], request_body=request_body + )[0] return updated_project_member def put_project_members(self, project_id: str, members: List[Member], delete: bool = False): @@ -98,22 +102,25 @@ def put_project_members(self, project_id: str, members: List[Member], delete: bo logger.warning(f"ユーザ '{member.user_id}' は、" f"'{organization_name}' 組織の組織メンバでないため、登録できませんでした。") continue - message_for_confirm = (f"ユーザ '{member.user_id}'を、{project_title} プロジェクトのメンバに登録しますか?" - f"member_role={member.member_role.value}") + message_for_confirm = ( + f"ユーザ '{member.user_id}'を、{project_title} プロジェクトのメンバに登録しますか?" f"member_role={member.member_role.value}" + ) if not self.confirm_processing(message_for_confirm): continue # メンバを登録 try: self.invite_project_member(project_id, member, old_project_members) - logger.debug(f"user_id = {member.user_id}, member_role = {member.member_role.value} のユーザをプ" - f"ロジェクトメンバに登録しました。") + logger.debug( + f"user_id = {member.user_id}, member_role = {member.member_role.value} のユーザをプ" f"ロジェクトメンバに登録しました。" + ) count_invite_members += 1 except requests.exceptions.HTTPError as e: logger.warning(e) - logger.warning(f"プロジェクトメンバの登録に失敗しました。" - f"user_id = {member.user_id}, member_role = {member.member_role.value}") + logger.warning( + f"プロジェクトメンバの登録に失敗しました。" f"user_id = {member.user_id}, member_role = {member.member_role.value}" + ) logger.info(f"{project_title} に、{count_invite_members} / {len(members)} 件のプロジェクトメンバを登録しました。") @@ -122,14 +129,15 @@ def put_project_members(self, project_id: str, members: List[Member], delete: bo user_id_list = [e.user_id for e in members] # 自分自身は削除しないようにする deleted_members = [ - e for e in old_project_members - if (e['user_id'] not in user_id_list and e['user_id'] != self.service.api.login_user_id) + e + for e in old_project_members + if (e["user_id"] not in user_id_list and e["user_id"] != self.service.api.login_user_id) ] count_delete_members = 0 logger.info(f"{project_title} から、{len(deleted_members)} 件のプロジェクトメンバを削除します。") for deleted_member in deleted_members: - message_for_confirm = (f"ユーザ '{deleted_member['user_id']}'を、" f"{project_title} のプロジェクトメンバから削除しますか?") + message_for_confirm = f"ユーザ '{deleted_member['user_id']}'を、" f"{project_title} のプロジェクトメンバから削除しますか?" if not self.confirm_processing(message_for_confirm): continue @@ -146,13 +154,19 @@ def put_project_members(self, project_id: str, members: List[Member], delete: bo @staticmethod def get_members_from_csv(csv_path: Path) -> List[Member]: def create_member(e): - return Member(user_id=e.user_id, member_role=ProjectMemberRole(e.member_role), - sampling_inspection_rate=e.sampling_inspection_rate, - sampling_acceptance_rate=e.sampling_acceptance_rate) - - df = pandas.read_csv(str(csv_path), sep=',', header=None, - names=('user_id', 'member_role', 'sampling_inspection_rate', - 'sampling_acceptance_rate')).replace({pandas.np.nan: None}) + return Member( + user_id=e.user_id, + member_role=ProjectMemberRole(e.member_role), + sampling_inspection_rate=e.sampling_inspection_rate, + sampling_acceptance_rate=e.sampling_acceptance_rate, + ) + + df = pandas.read_csv( + str(csv_path), + sep=",", + header=None, + names=("user_id", "member_role", "sampling_inspection_rate", "sampling_acceptance_rate"), + ).replace({pandas.np.nan: None}) members = [create_member(e) for e in df.itertuples()] return members @@ -174,15 +188,20 @@ def parse_args(parser: argparse.ArgumentParser): argument_parser.add_project_id() parser.add_argument( - '--csv', type=str, required=True, - help=('プロジェクトメンバが記載されたCVファイルのパスを指定してください。' - 'CSVのフォーマットは、「1列目:user_id(required), 2列目:member_role(required), ' - '3列目:sampling_inspection_rate, 4列目:sampling_acceptance_rate, ヘッダ行なし, カンマ区切り」です。' - 'member_roleは `owner`, `worker`, `accepter`, `training_data_user` のいずれかです。' - 'sampling_inspection_rate, sampling_acceptance_rate を省略した場合は未設定になります。' - 'ただし自分自身は登録しません。')) - - parser.add_argument('--delete', action='store_true', help='CSVファイルに記載されていないプロジェクトメンバを削除します。ただし自分自身は削除しません。') + "--csv", + type=str, + required=True, + help=( + "プロジェクトメンバが記載されたCVファイルのパスを指定してください。" + "CSVのフォーマットは、「1列目:user_id(required), 2列目:member_role(required), " + "3列目:sampling_inspection_rate, 4列目:sampling_acceptance_rate, ヘッダ行なし, カンマ区切り」です。" + "member_roleは `owner`, `worker`, `accepter`, `training_data_user` のいずれかです。" + "sampling_inspection_rate, sampling_acceptance_rate を省略した場合は未設定になります。" + "ただし自分自身は登録しません。" + ), + ) + + parser.add_argument("--delete", action="store_true", help="CSVファイルに記載されていないプロジェクトメンバを削除します。ただし自分自身は削除しません。") parser.set_defaults(subcommand_func=main) @@ -190,7 +209,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "put" subcommand_help = "プロジェクトメンバを登録する。" - description = ("プロジェクトメンバを登録する。") + description = "プロジェクトメンバを登録する。" epilog = "オーナロールを持つユーザで実行してください。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog) diff --git a/annofabcli/project_member/subcommand_project_member.py b/annofabcli/project_member/subcommand_project_member.py index d18403a7..14a22f2d 100644 --- a/annofabcli/project_member/subcommand_project_member.py +++ b/annofabcli/project_member/subcommand_project_member.py @@ -12,7 +12,7 @@ def parse_args(parser: argparse.ArgumentParser): - subparsers = parser.add_subparsers(dest='subcommand_name') + subparsers = parser.add_subparsers(dest="subcommand_name") # サブコマンドの定義 annofabcli.project_member.change_project_members.add_parser(subparsers) diff --git a/annofabcli/statistics/database.py b/annofabcli/statistics/database.py index 5aaafc4f..178564e0 100644 --- a/annofabcli/statistics/database.py +++ b/annofabcli/statistics/database.py @@ -110,8 +110,9 @@ def task_exists(task_list: List[Task], task_id) -> bool: else: return True - def read_annotation_summary(self, task_list: List[Task], - annotation_summary_func: AnnotationSummaryFunc) -> AnnotationDict: + def read_annotation_summary( + self, task_list: List[Task], annotation_summary_func: AnnotationSummaryFunc + ) -> AnnotationDict: logger.debug(f"reading {str(self.annotations_zip_path)}") def read_annotation_summary(task_id: str, input_data_id_: str) -> Dict[str, Any]: @@ -218,10 +219,8 @@ def update_annotation_zip(self, project_id: str, should_update_annotation_zip: b logger.debug(f"アノテーション仕様の最終更新日時={annotation_specs_updated_datetime}") job_list = self.annofab_service.api.get_project_job( - project_id, query_params={ - "type": JobType.GEN_ANNOTATION.value, - "limit": 1 - })[0]["list"] + project_id, query_params={"type": JobType.GEN_ANNOTATION.value, "limit": 1} + )[0]["list"] if len(job_list) > 0: job = job_list[0] logger.debug(f"アノテーションzipの最終更新日時={job['updated_datetime']}, job_status={job['job_status']}") @@ -238,14 +237,16 @@ def update_annotation_zip(self, project_id: str, should_update_annotation_zip: b self.wait_for_completion_updated_annotation(project_id) elif job_status == JobStatus.SUCCEEDED: - if dateutil.parser.parse( - job["updated_datetime"]) < dateutil.parser.parse(last_tasks_updated_datetime): + if dateutil.parser.parse(job["updated_datetime"]) < dateutil.parser.parse( + last_tasks_updated_datetime + ): logger.info(f"タスクの最新更新日時よりアノテーションzipの最終更新日時の方が古いので、アノテーションzipを更新します。") self.annofab_service.api.post_annotation_archive_update(project_id) self.wait_for_completion_updated_annotation(project_id) - elif dateutil.parser.parse( - job["updated_datetime"]) < dateutil.parser.parse(annotation_specs_updated_datetime): + elif dateutil.parser.parse(job["updated_datetime"]) < dateutil.parser.parse( + annotation_specs_updated_datetime + ): logger.info(f"アノテーション仕様の更新日時よりアノテーションzipの最終更新日時の方が古いので、アノテーションzipを更新します。") self.annofab_service.api.post_annotation_archive_update(project_id) self.wait_for_completion_updated_annotation(project_id) @@ -268,10 +269,8 @@ def update_task_json(self, project_id: str, should_update_task_json: bool = Fals """ job_list = self.annofab_service.api.get_project_job( - project_id, query_params={ - "type": JobType.GEN_TASKS_LIST.value, - "limit": 1 - })[0]["list"] + project_id, query_params={"type": JobType.GEN_TASKS_LIST.value, "limit": 1} + )[0]["list"] if len(job_list) == 0: if should_update_task_json: @@ -298,9 +297,7 @@ def update_task_json(self, project_id: str, should_update_task_json: bool = Fals self.wait_for_completion_updated_task_json(project_id) def _download_db_file( - self, - should_update_annotation_zip: bool = False, - should_update_task_json: bool = False, + self, should_update_annotation_zip: bool = False, should_update_task_json: bool = False, ): """ DBになりうるファイルをダウンロードする @@ -326,9 +323,7 @@ def _download_db_file( # task historiesは未完成なので、使わない def read_tasks_from_json( - self, - task_query_param: Optional[Dict[str, Any]] = None, - ignored_task_id_list: Optional[List[str]] = None, + self, task_query_param: Optional[Dict[str, Any]] = None, ignored_task_id_list: Optional[List[str]] = None, ) -> List[Task]: """ tass.jsonからqueryに合致するタスクを調べる @@ -339,6 +334,7 @@ def read_tasks_from_json( Returns: """ + def filter_task(arg_task): """AND条件で絞り込む""" @@ -364,11 +360,11 @@ def filter_task(arg_task): return filtered_task_list def update_db( - self, - task_query_param: Dict[str, Any], - ignored_task_ids: Optional[List[str]] = None, - should_update_annotation_zip: bool = False, - should_update_task_json: bool = False, + self, + task_query_param: Dict[str, Any], + ignored_task_ids: Optional[List[str]] = None, + should_update_annotation_zip: bool = False, + should_update_task_json: bool = False, ) -> None: """ Annofabから情報を取得し、DB(pickelファイル)を更新する。 @@ -385,8 +381,7 @@ def update_db( # DB用のJSONファイルをダウンロードする self._download_db_file( - should_update_annotation_zip=should_update_annotation_zip, - should_update_task_json=should_update_task_json, + should_update_annotation_zip=should_update_annotation_zip, should_update_task_json=should_update_task_json, ) logger.info(f"DB更新: task_query_param = {task_query_param}") @@ -427,13 +422,15 @@ def get_not_updated_task_ids(old_tasks, new_tasks) -> Set[str]: continue old_task = filterd_list[0] if dateutil.parser.parse(old_task["updated_datetime"]) == dateutil.parser.parse( - new_task["updated_datetime"]): + new_task["updated_datetime"] + ): updated_task_ids.add(new_task["task_id"]) return updated_task_ids - def get_task_histories_dict(self, all_tasks: List[Task], - ignored_task_ids: Optional[Set[str]] = None) -> Dict[str, List[TaskHistory]]: + def get_task_histories_dict( + self, all_tasks: List[Task], ignored_task_ids: Optional[Set[str]] = None + ) -> Dict[str, List[TaskHistory]]: """ Annofabからタスク履歴情報を取得する。 Args: diff --git a/annofabcli/statistics/graph.py b/annofabcli/statistics/graph.py index 73eca71e..058eaa2a 100644 --- a/annofabcli/statistics/graph.py +++ b/annofabcli/statistics/graph.py @@ -66,10 +66,7 @@ def _create_hover_tool(tool_tip_items: List[str] = None) -> HoverTool: detail_tooltips = [(e, "@{%s}" % e) for e in tool_tip_items] - hover_tool = HoverTool(tooltips=[ - ("index", "$index"), - ("(x,y)", "($x, $y)"), - ] + detail_tooltips) + hover_tool = HoverTool(tooltips=[("index", "$index"), ("(x,y)", "($x, $y)"),] + detail_tooltips) return hover_tool @staticmethod @@ -99,13 +96,7 @@ def _plot_line_and_circle(fig, x, y, source: ColumnDataSource, username: str, co muted_color=color, ) fig.circle( - x=x, - y=y, - source=source, - legend_label=username, - muted_alpha=0.0, - muted_color=color, - color=color, + x=x, y=y, source=source, legend_label=username, muted_alpha=0.0, muted_color=color, color=color, ) @staticmethod @@ -145,10 +136,11 @@ def _create_histogram(df: pd.DataFrame, histogram_name: HistogramName, bins: int data = df[histogram_name.column].values frequencies, edges = np.histogram(data, bins) - hist = hv.Histogram((edges, frequencies), kdims=histogram_name.x_axis_label, - vdims=histogram_name.y_axis_label).options(width=500, title=title, fontsize={ - "title": 9 - }).opts(hv.opts(tools=["hover"])) + hist = ( + hv.Histogram((edges, frequencies), kdims=histogram_name.x_axis_label, vdims=histogram_name.y_axis_label) + .options(width=500, title=title, fontsize={"title": 9}) + .opts(hv.opts(tools=["hover"])) + ) return hist def write_histogram_for_worktime(self, df: pd.DataFrame): @@ -166,51 +158,21 @@ def write_histogram_for_worktime(self, df: pd.DataFrame): renderer = hv.renderer("bokeh") histogram_name_list = [ + HistogramName(column="annotation_worktime_hour", x_axis_label="教師付時間[hour]", title="教師付時間",), + HistogramName(column="inspection_worktime_hour", x_axis_label="検査時間[hour]", title="検査時間",), + HistogramName(column="acceptance_worktime_hour", x_axis_label="受入時間[hour]", title="受入時間",), HistogramName( - column="annotation_worktime_hour", - x_axis_label="教師付時間[hour]", - title="教師付時間", - ), - HistogramName( - column="inspection_worktime_hour", - x_axis_label="検査時間[hour]", - title="検査時間", - ), - HistogramName( - column="acceptance_worktime_hour", - x_axis_label="受入時間[hour]", - title="受入時間", + column="first_annotator_worktime_hour", x_axis_label="1回目の教師付者の作業時間[hour]", title="1回目の教師付者の作業時間", ), HistogramName( - column="first_annotator_worktime_hour", - x_axis_label="1回目の教師付者の作業時間[hour]", - title="1回目の教師付者の作業時間", + column="first_inspector_worktime_hour", x_axis_label="1回目の検査者の作業時間[hour]", title="1回目の検査者の作業時間", ), HistogramName( - column="first_inspector_worktime_hour", - x_axis_label="1回目の検査者の作業時間[hour]", - title="1回目の検査者の作業時間", - ), - HistogramName( - column="first_acceptor_worktime_hour", - x_axis_label="1回目の受入者の作業時間[hour]", - title="1回目の受入者の作業時間", - ), - HistogramName( - column="first_annotation_worktime_hour", - x_axis_label="1回目の教師付時間[hour]", - title="1回目の教師付時間", - ), - HistogramName( - column="first_inspection_worktime_hour", - x_axis_label="1回目の検査時間[hour]", - title="1回目の検査時間", - ), - HistogramName( - column="first_acceptance_worktime_hour", - x_axis_label="1回目の受入時間[hour]", - title="1回目の受入時間", + column="first_acceptor_worktime_hour", x_axis_label="1回目の受入者の作業時間[hour]", title="1回目の受入者の作業時間", ), + HistogramName(column="first_annotation_worktime_hour", x_axis_label="1回目の教師付時間[hour]", title="1回目の教師付時間",), + HistogramName(column="first_inspection_worktime_hour", x_axis_label="1回目の検査時間[hour]", title="1回目の検査時間",), + HistogramName(column="first_acceptance_worktime_hour", x_axis_label="1回目の受入時間[hour]", title="1回目の受入時間",), HistogramName(column="sum_worktime_hour", x_axis_label="総作業時間[hour]", title="総作業時間"), ] @@ -226,11 +188,10 @@ def write_histogram_for_worktime(self, df: pd.DataFrame): self._create_histogram( filtered_df[~filtered_df["acceptance_is_skipped"]], histogram_name=HistogramName( - column="acceptance_worktime_hour", - x_axis_label="受入時間[hour]", - title="受入時間(自動受入されたタスクを除外)", + column="acceptance_worktime_hour", x_axis_label="受入時間[hour]", title="受入時間(自動受入されたタスクを除外)", ), - )) + ) + ) # 軸範囲が同期しないようにする layout = hv.Layout(histograms).options(shared_axes=False).cols(3) @@ -254,38 +215,21 @@ def write_histogram_for_other(self, df: pd.DataFrame): HistogramName(column="annotation_count", x_axis_label="アノテーション数", title="アノテーション数"), HistogramName(column="input_data_count", x_axis_label="画像枚数", title="画像枚数"), HistogramName(column="inspection_count", x_axis_label="検査コメント数", title="検査コメント数"), - HistogramName( - column="input_data_count_of_inspection", - x_axis_label="指摘を受けた画像枚数", - title="指摘を受けた画像枚数", - ), - + HistogramName(column="input_data_count_of_inspection", x_axis_label="指摘を受けた画像枚数", title="指摘を受けた画像枚数",), # 経過日数 HistogramName( - column="diff_days_to_first_inspection_started", - x_axis_label="最初の検査を着手するまでの日数", - title="最初の検査を着手するまでの日数", - ), - HistogramName( - column="diff_days_to_first_acceptance_started", - x_axis_label="最初の受入を着手するまでの日数", - title="最初の受入を着手するまでの日数", + column="diff_days_to_first_inspection_started", x_axis_label="最初の検査を着手するまでの日数", title="最初の検査を着手するまでの日数", ), HistogramName( - column="diff_days_to_task_completed", - x_axis_label="受入完了状態になるまでの日数", - title="受入完了状態になるまでの日数", + column="diff_days_to_first_acceptance_started", x_axis_label="最初の受入を着手するまでの日数", title="最初の受入を着手するまでの日数", ), + HistogramName(column="diff_days_to_task_completed", x_axis_label="受入完了状態になるまでの日数", title="受入完了状態になるまでの日数",), # 差し戻し回数 HistogramName( - column="number_of_rejections_by_inspection", - x_axis_label="検査フェーズでの差し戻し回数", - title="検査フェーズでの差し戻し回数", + column="number_of_rejections_by_inspection", x_axis_label="検査フェーズでの差し戻し回数", title="検査フェーズでの差し戻し回数", ), HistogramName( - column="number_of_rejections_by_acceptance", - x_axis_label="受入フェーズでの差し戻し回数", - title="受入フェーズでの差し戻し回数", + column="number_of_rejections_by_acceptance", x_axis_label="受入フェーズでの差し戻し回数", title="受入フェーズでの差し戻し回数", ), ] @@ -313,9 +257,10 @@ def write_histogram_for_annotation_count_by_label(self, df: pd.DataFrame) -> Non label_columns = [e for e in df.columns if e.startswith("label_")] for column in label_columns: - label_name = column[len("label_"):] - histogram_name = HistogramName(column=column, x_axis_label=f"'{label_name}'のアノテーション数", - title=f"{label_name}") + label_name = column[len("label_") :] + histogram_name = HistogramName( + column=column, x_axis_label=f"'{label_name}'のアノテーション数", title=f"{label_name}" + ) hist = self._create_histogram(df, histogram_name=histogram_name) histograms.append(hist) @@ -325,10 +270,7 @@ def write_histogram_for_annotation_count_by_label(self, df: pd.DataFrame) -> Non renderer.save(layout, f"{self.outdir}/html/{self.short_project_id}-ヒストグラム-ラベルごとのアノテーション数") def create_user_id_list( - self, - df: pd.DataFrame, - user_id_column: str, - arg_user_id_list: Optional[List[str]] = None, + self, df: pd.DataFrame, user_id_column: str, arg_user_id_list: Optional[List[str]] = None, ) -> List[str]: """ グラフに表示するユーザのuser_idを生成する。 @@ -356,9 +298,7 @@ def create_user_id_list( return user_id_list[0:max_user_length] def write_productivity_line_graph_for_annotator( - self, - df: pd.DataFrame, - first_annotation_user_id_list: Optional[List[str]] = None, + self, df: pd.DataFrame, first_annotation_user_id_list: Optional[List[str]] = None, ): """ 生産性を教師付作業者ごとにプロットする。 @@ -370,6 +310,7 @@ def write_productivity_line_graph_for_annotator( Returns: """ + def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str): """ @@ -390,7 +331,8 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) x_axis_label=fig_info["x_axis_label"], x_axis_type="datetime", y_axis_label=fig_info["y_axis_label"], - )) + ) + ) for user_index, user_id in enumerate(first_annotation_user_id_list): # type: ignore filtered_df = df[df["first_annotation_user_id"] == user_id] @@ -404,12 +346,7 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) for fig, fig_info in zip(figs, fig_info_list): self._plot_line_and_circle( - fig, - x=fig_info["x"], - y=fig_info["y"], - source=source, - username=username, - color=color, + fig, x=fig_info["x"], y=fig_info["y"], source=source, username=username, color=color, ) hover_tool = self._create_hover_tool(tooltip_item) @@ -418,8 +355,7 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) bokeh.plotting.reset_output() bokeh.plotting.output_file( - f"{self.outdir}/html/{self.short_project_id}-{html_title}.html", - title=html_title, + f"{self.outdir}/html/{self.short_project_id}-{html_title}.html", title=html_title, ) bokeh.plotting.save(bokeh.layouts.column(figs)) @@ -441,12 +377,14 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) logger.info("データが0件のため出力しない") return - first_annotation_user_id_list = self.create_user_id_list(df, "first_annotation_user_id", - first_annotation_user_id_list) + first_annotation_user_id_list = self.create_user_id_list( + df, "first_annotation_user_id", first_annotation_user_id_list + ) logger.debug(f"グラフに表示するuser_id = {first_annotation_user_id_list}") df["date_first_annotation_started_date"] = df["first_annotation_started_date"].map( - lambda e: dateutil.parser.parse(e).date()) + lambda e: dateutil.parser.parse(e).date() + ) fig_info_list_annotation_count = [ dict( @@ -486,8 +424,7 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) ), ] write_cumulative_graph( - fig_info_list_annotation_count, - html_title="折れ線-横軸_教師付開始日-縦軸_アノテーションあたりの指標-教師付者用", + fig_info_list_annotation_count, html_title="折れ線-横軸_教師付開始日-縦軸_アノテーションあたりの指標-教師付者用", ) fig_info_list_input_data_count = [ @@ -569,9 +506,7 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) write_cumulative_graph(fig_info_list_value, html_title="折れ線-横軸_教師付開始日-縦軸_指標-教師付者用") def write_cumulative_line_graph_for_annotator( - self, - df: pd.DataFrame, - first_annotation_user_id_list: Optional[List[str]] = None, + self, df: pd.DataFrame, first_annotation_user_id_list: Optional[List[str]] = None, ): """ 教師付作業者用の累積折れ線グラフを出力する。 @@ -583,6 +518,7 @@ def write_cumulative_line_graph_for_annotator( Returns: """ + def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str): """ 累計グラフを出力する。 @@ -603,7 +539,8 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) title=fig_info["title"], x_axis_label=fig_info["x_axis_label"], y_axis_label=fig_info["y_axis_label"], - )) + ) + ) for user_index, user_id in enumerate(first_annotation_user_id_list): # type: ignore filtered_df = df[df["first_annotation_user_id"] == user_id] @@ -632,12 +569,7 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) for fig, fig_info in zip(figs, fig_info_list): self._plot_line_and_circle( - fig, - x=fig_info["x"], - y=fig_info["y"], - source=source, - username=username, - color=color, + fig, x=fig_info["x"], y=fig_info["y"], source=source, username=username, color=color, ) hover_tool = self._create_hover_tool(tooltip_item) @@ -646,8 +578,7 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) bokeh.plotting.reset_output() bokeh.plotting.output_file( - f"{self.outdir}/html/{self.short_project_id}-{html_title}.html", - title=html_title, + f"{self.outdir}/html/{self.short_project_id}-{html_title}.html", title=html_title, ) bokeh.plotting.save(bokeh.layouts.column(figs)) @@ -671,8 +602,9 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) logger.info("タスク一覧が0件のため出力しない") return - first_annotation_user_id_list = self.create_user_id_list(df, "first_annotation_user_id", - first_annotation_user_id_list) + first_annotation_user_id_list = self.create_user_id_list( + df, "first_annotation_user_id", first_annotation_user_id_list + ) logger.debug(f"グラフに表示するuser_id = {first_annotation_user_id_list}") # 横軸が累計のアノテーション数 @@ -796,9 +728,7 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) write_cumulative_graph(fig_info_list_task_count, html_title="累計折れ線-横軸_タスク数-教師付者用") def write_cumulative_line_graph_for_inspector( - self, - df: pd.DataFrame, - first_inspection_user_id_list: Optional[List[str]] = None, + self, df: pd.DataFrame, first_inspection_user_id_list: Optional[List[str]] = None, ): """ 検査作業者用の累積折れ線グラフを出力する。 @@ -810,6 +740,7 @@ def write_cumulative_line_graph_for_inspector( Returns: """ + def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str): """ 累計グラフを出力する。 @@ -830,7 +761,8 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) title=fig_info["title"], x_axis_label=fig_info["x_axis_label"], y_axis_label=fig_info["y_axis_label"], - )) + ) + ) for user_index, user_id in enumerate(first_inspection_user_id_list): # type: ignore filtered_df = df[df["first_inspection_user_id"] == user_id] @@ -856,12 +788,7 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) for fig, fig_info in zip(figs, fig_info_list): self._plot_line_and_circle( - fig, - x=fig_info["x"], - y=fig_info["y"], - source=source, - username=username, - color=color, + fig, x=fig_info["x"], y=fig_info["y"], source=source, username=username, color=color, ) hover_tool = self._create_hover_tool(tooltip_item) @@ -870,8 +797,7 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) bokeh.plotting.reset_output() bokeh.plotting.output_file( - f"{self.outdir}/html/{self.short_project_id}-{html_title}.html", - title=html_title, + f"{self.outdir}/html/{self.short_project_id}-{html_title}.html", title=html_title, ) bokeh.plotting.save(bokeh.layouts.column(figs)) @@ -896,8 +822,9 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) logger.info("タスク一覧が0件のため出力しない") return - first_inspection_user_id_list = self.create_user_id_list(df, "first_inspection_user_id", - first_inspection_user_id_list) + first_inspection_user_id_list = self.create_user_id_list( + df, "first_inspection_user_id", first_inspection_user_id_list + ) if len(first_inspection_user_id_list) == 0: logger.info(f"検査フェーズを担当してユーザがいないため、検査者用のグラフは出力しません。") @@ -958,9 +885,7 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) write_cumulative_graph(fig_info_list_input_data_count, html_title="累計折れ線-横軸_入力データ数-検査者用") def write_cumulative_line_graph_for_acceptor( - self, - df: pd.DataFrame, - first_acceptance_user_id_list: Optional[List[str]] = None, + self, df: pd.DataFrame, first_acceptance_user_id_list: Optional[List[str]] = None, ): """ 受入者用の累積折れ線グラフを出力する。 @@ -972,6 +897,7 @@ def write_cumulative_line_graph_for_acceptor( Returns: """ + def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str): """ 累計グラフを出力する。 @@ -992,7 +918,8 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) title=fig_info["title"], x_axis_label=fig_info["x_axis_label"], y_axis_label=fig_info["y_axis_label"], - )) + ) + ) for user_index, user_id in enumerate(first_acceptance_user_id_list): # type: ignore filtered_df = df[df["first_acceptance_user_id"] == user_id] @@ -1017,12 +944,7 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) for fig, fig_info in zip(figs, fig_info_list): self._plot_line_and_circle( - fig, - x=fig_info["x"], - y=fig_info["y"], - source=source, - username=username, - color=color, + fig, x=fig_info["x"], y=fig_info["y"], source=source, username=username, color=color, ) hover_tool = self._create_hover_tool(tooltip_item) @@ -1031,8 +953,7 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) bokeh.plotting.reset_output() bokeh.plotting.output_file( - f"{self.outdir}/html/{self.short_project_id}-{html_title}.html", - title=html_title, + f"{self.outdir}/html/{self.short_project_id}-{html_title}.html", title=html_title, ) bokeh.plotting.save(bokeh.layouts.column(figs)) @@ -1059,8 +980,9 @@ def write_cumulative_graph(fig_info_list: List[Dict[str, str]], html_title: str) logger.info("タスク一覧が0件のため出力しない") return - first_acceptance_user_id_list = self.create_user_id_list(df, "first_acceptance_user_id", - first_acceptance_user_id_list) + first_acceptance_user_id_list = self.create_user_id_list( + df, "first_acceptance_user_id", first_acceptance_user_id_list + ) if len(first_acceptance_user_id_list) == 0: logger.info(f"受入フェーズを担当してユーザがいないため、受入者用のグラフは出力しません。") diff --git a/annofabcli/statistics/table.py b/annofabcli/statistics/table.py index 5ffd4b2c..99783a52 100644 --- a/annofabcli/statistics/table.py +++ b/annofabcli/statistics/table.py @@ -6,8 +6,12 @@ import dateutil import pandas as pd from annofabapi.dataclass.annotation import SimpleAnnotationDetail -from annofabapi.dataclass.statistics import (ProjectAccountStatistics, ProjectAccountStatisticsHistory, - WorktimeStatistics, WorktimeStatisticsItem) +from annofabapi.dataclass.statistics import ( + ProjectAccountStatistics, + ProjectAccountStatisticsHistory, + WorktimeStatistics, + WorktimeStatisticsItem, +) from annofabapi.models import InputDataId, Inspection, InspectionStatus, Task, TaskHistory, TaskPhase, TaskStatus from more_itertools import first_true @@ -54,10 +58,7 @@ class Table: _account_statistics: Optional[List[ProjectAccountStatistics]] = None def __init__( - self, - database: Database, - task_query_param: Dict[str, Any], - ignored_task_id_list: Optional[List[str]] = None, + self, database: Database, task_query_param: Dict[str, Any], ignored_task_id_list: Optional[List[str]] = None, ): self.annofab_service = database.annofab_service self.annofab_facade = AnnofabApiFacade(database.annofab_service) @@ -318,8 +319,9 @@ def get_rejections_by_phase(task_histories: List[TaskHistory], phase: TaskPhase) return rejections_by_phase - def _set_first_phase_from_task_history(self, task: Task, task_history: Optional[TaskHistory], - column_prefix: str) -> Task: + def _set_first_phase_from_task_history( + self, task: Task, task_history: Optional[TaskHistory], column_prefix: str + ) -> Task: """ 最初のフェーズに関する情報をTask情報に設定する。 @@ -332,23 +334,28 @@ def _set_first_phase_from_task_history(self, task: Task, task_history: Optional[ """ if task_history is None: - task.update({ - f"{column_prefix}_account_id": None, - f"{column_prefix}_user_id": None, - f"{column_prefix}_username": None, - f"{column_prefix}_started_datetime": None, - f"{column_prefix}_worktime_hour": 0, - }) + task.update( + { + f"{column_prefix}_account_id": None, + f"{column_prefix}_user_id": None, + f"{column_prefix}_username": None, + f"{column_prefix}_started_datetime": None, + f"{column_prefix}_worktime_hour": 0, + } + ) else: account_id = task_history["account_id"] - task.update({ - f"{column_prefix}_account_id": account_id, - f"{column_prefix}_user_id": self._get_user_id(account_id), - f"{column_prefix}_username": self._get_username(account_id), - f"{column_prefix}_started_datetime": task_history["started_datetime"], - f"{column_prefix}_worktime_hour": isoduration_to_hour( - task_history["accumulated_labor_time_milliseconds"]), - }) + task.update( + { + f"{column_prefix}_account_id": account_id, + f"{column_prefix}_user_id": self._get_user_id(account_id), + f"{column_prefix}_username": self._get_username(account_id), + f"{column_prefix}_started_datetime": task_history["started_datetime"], + f"{column_prefix}_worktime_hour": isoduration_to_hour( + task_history["accumulated_labor_time_milliseconds"] + ), + } + ) return task @@ -365,16 +372,19 @@ def _get_first_operator_worktime(task_histories: List[TaskHistory], account_id: 作業時間 """ - return sum([ - isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) - for e in task_histories - if e["account_id"] == account_id - ]) + return sum( + [ + isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) + for e in task_histories + if e["account_id"] == account_id + ] + ) def set_task_histories(self, task: Task, task_histories: List[TaskHistory]): """ タスク履歴関係の情報を設定する """ + def diff_days(ended_key: str, started_key: str) -> Optional[float]: if task[ended_key] is not None and task[started_key] is not None: delta = dateutil.parser.parse(task[ended_key]) - dateutil.parser.parse(task[started_key]) @@ -384,9 +394,13 @@ def diff_days(ended_key: str, started_key: str) -> Optional[float]: def acceptance_is_skipped(arg_task_histories: List[TaskHistory]) -> bool: skipped_histories = [ - e for e in arg_task_histories - if (e["phase"] == TaskPhase.ACCEPTANCE.value and e["account_id"] is None - and annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) == 0) + e + for e in arg_task_histories + if ( + e["phase"] == TaskPhase.ACCEPTANCE.value + and e["account_id"] is None + and annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) == 0 + ) ] return len(skipped_histories) > 0 @@ -406,31 +420,47 @@ def acceptance_is_skipped(arg_task_histories: List[TaskHistory]) -> bool: first_acceptance_history = acceptance_histories[0] if len(acceptance_histories) > 0 else None self._set_first_phase_from_task_history(task, first_acceptance_history, column_prefix="first_acceptance") - task["annotation_worktime_hour"] = sum([ - annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) for e in annotation_histories - ]) - task["inspection_worktime_hour"] = sum([ - annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) for e in inspection_histories - ]) - task["acceptance_worktime_hour"] = sum([ - annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) for e in acceptance_histories - ]) + task["annotation_worktime_hour"] = sum( + [ + annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) + for e in annotation_histories + ] + ) + task["inspection_worktime_hour"] = sum( + [ + annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) + for e in inspection_histories + ] + ) + task["acceptance_worktime_hour"] = sum( + [ + annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) + for e in acceptance_histories + ] + ) # 最初の教師者が担当した履歴の合計作業時間を取得する。 # 担当者変更がなければ、"annotation_worktime_hour"と"first_annotation_worktime_hour"は同じ値 - task["first_annotator_worktime_hour"] = (self._get_first_operator_worktime( - annotation_histories, first_annotation_history["account_id"]) - if first_annotation_history is not None else 0) + task["first_annotator_worktime_hour"] = ( + self._get_first_operator_worktime(annotation_histories, first_annotation_history["account_id"]) + if first_annotation_history is not None + else 0 + ) - task["first_inspector_worktime_hour"] = (self._get_first_operator_worktime( - inspection_histories, first_inspection_history["account_id"]) - if first_inspection_history is not None else 0) - task["first_acceptor_worktime_hour"] = (self._get_first_operator_worktime( - acceptance_histories, first_acceptance_history["account_id"]) - if first_acceptance_history is not None else 0) + task["first_inspector_worktime_hour"] = ( + self._get_first_operator_worktime(inspection_histories, first_inspection_history["account_id"]) + if first_inspection_history is not None + else 0 + ) + task["first_acceptor_worktime_hour"] = ( + self._get_first_operator_worktime(acceptance_histories, first_acceptance_history["account_id"]) + if first_acceptance_history is not None + else 0 + ) task["sum_worktime_hour"] = sum( - [annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) for e in task_histories]) + [annofabcli.utils.isoduration_to_hour(e["accumulated_labor_time_milliseconds"]) for e in task_histories] + ) task["number_of_rejections_by_inspection"] = self.get_rejections_by_phase(task_histories, TaskPhase.INSPECTION) task["number_of_rejections_by_acceptance"] = self.get_rejections_by_phase(task_histories, TaskPhase.ACCEPTANCE) @@ -442,10 +472,12 @@ def acceptance_is_skipped(arg_task_histories: List[TaskHistory]) -> bool: else: task["task_completed_datetime"] = None - task["diff_days_to_first_inspection_started"] = diff_days("first_inspection_started_datetime", - "first_annotation_started_datetime") - task["diff_days_to_first_acceptance_started"] = diff_days("first_acceptance_started_datetime", - "first_annotation_started_datetime") + task["diff_days_to_first_inspection_started"] = diff_days( + "first_inspection_started_datetime", "first_annotation_started_datetime" + ) + task["diff_days_to_first_acceptance_started"] = diff_days( + "first_acceptance_started_datetime", "first_annotation_started_datetime" + ) task["diff_days_to_task_completed"] = diff_days("task_completed_datetime", "first_annotation_started_datetime") @@ -470,7 +502,8 @@ def create_task_history_df(self) -> pd.DataFrame: history["user_id"] = self._get_user_id(account_id) history["username"] = self._get_username(account_id) history["worktime_hour"] = annofabcli.utils.isoduration_to_hour( - history["accumulated_labor_time_milliseconds"]) + history["accumulated_labor_time_milliseconds"] + ) all_task_history_list.append(history) df = pd.DataFrame(all_task_history_list) @@ -483,6 +516,7 @@ def create_task_df(self) -> pd.DataFrame: first_annotation_user_id, first_annotation_started_datetime, annotation_worktime_hour, inspection_worktime_hour, acceptance_worktime_hour, sum_worktime_hour """ + def set_annotation_info(arg_task): total_annotation_count = 0 @@ -505,7 +539,8 @@ def set_inspection_info(arg_task): for inspection_list in input_data_dict.values(): # 検査コメントを絞り込む filtered_inspection_list = [ - e for e in inspection_list + e + for e in inspection_list if self._inspection_condition(e, exclude_reply=True, only_error_corrected=True) ] inspection_count += len(filtered_inspection_list) @@ -585,45 +620,55 @@ def create_dataframe_by_date(task_df: pd.DataFrame) -> pd.DataFrame: """ new_df = task_df new_df["first_annotation_started_date"] = new_df["first_annotation_started_datetime"].map( - lambda e: datetime_to_date(e) if e is not None else None) + lambda e: datetime_to_date(e) if e is not None else None + ) new_df["task_count"] = 1 # 集計用 # first_annotation_user_id と first_annotation_usernameの両方を指定している理由: # first_annotation_username を取得するため group_obj = new_df.groupby( - ["first_annotation_started_date", "first_annotation_user_id", "first_annotation_username"], - as_index=False, + ["first_annotation_started_date", "first_annotation_user_id", "first_annotation_username"], as_index=False, + ) + sum_df = group_obj[ + [ + "first_annotation_worktime_hour", + "annotation_worktime_hour", + "inspection_worktime_hour", + "acceptance_worktime_hour", + "sum_worktime_hour", + "task_count", + "input_data_count", + "annotation_count", + "inspection_count", + ] + ].sum() + + sum_df["first_annotation_worktime_minute/annotation_count"] = ( + sum_df["first_annotation_worktime_hour"] * 60 / sum_df["annotation_count"] + ) + sum_df["annotation_worktime_minute/annotation_count"] = ( + sum_df["annotation_worktime_hour"] * 60 / sum_df["annotation_count"] + ) + sum_df["inspection_worktime_minute/annotation_count"] = ( + sum_df["inspection_worktime_hour"] * 60 / sum_df["annotation_count"] + ) + sum_df["acceptance_worktime_minute/annotation_count"] = ( + sum_df["acceptance_worktime_hour"] * 60 / sum_df["annotation_count"] ) - sum_df = group_obj[[ - "first_annotation_worktime_hour", - "annotation_worktime_hour", - "inspection_worktime_hour", - "acceptance_worktime_hour", - "sum_worktime_hour", - "task_count", - "input_data_count", - "annotation_count", - "inspection_count", - ]].sum() - - sum_df["first_annotation_worktime_minute/annotation_count"] = (sum_df["first_annotation_worktime_hour"] * 60 / - sum_df["annotation_count"]) - sum_df["annotation_worktime_minute/annotation_count"] = (sum_df["annotation_worktime_hour"] * 60 / - sum_df["annotation_count"]) - sum_df["inspection_worktime_minute/annotation_count"] = (sum_df["inspection_worktime_hour"] * 60 / - sum_df["annotation_count"]) - sum_df["acceptance_worktime_minute/annotation_count"] = (sum_df["acceptance_worktime_hour"] * 60 / - sum_df["annotation_count"]) sum_df["inspection_count/annotation_count"] = sum_df["inspection_count"] / sum_df["annotation_count"] - sum_df["first_annotation_worktime_minute/input_data_count"] = (sum_df["first_annotation_worktime_hour"] * 60 / - sum_df["input_data_count"]) - sum_df["annotation_worktime_minute/input_data_count"] = (sum_df["annotation_worktime_hour"] * 60 / - sum_df["input_data_count"]) - sum_df["inspection_worktime_minute/input_data_count"] = (sum_df["inspection_worktime_hour"] * 60 / - sum_df["input_data_count"]) - sum_df["acceptance_worktime_minute/input_data_count"] = (sum_df["acceptance_worktime_hour"] * 60 / - sum_df["input_data_count"]) + sum_df["first_annotation_worktime_minute/input_data_count"] = ( + sum_df["first_annotation_worktime_hour"] * 60 / sum_df["input_data_count"] + ) + sum_df["annotation_worktime_minute/input_data_count"] = ( + sum_df["annotation_worktime_hour"] * 60 / sum_df["input_data_count"] + ) + sum_df["inspection_worktime_minute/input_data_count"] = ( + sum_df["inspection_worktime_hour"] * 60 / sum_df["input_data_count"] + ) + sum_df["acceptance_worktime_minute/input_data_count"] = ( + sum_df["acceptance_worktime_hour"] * 60 / sum_df["input_data_count"] + ) sum_df["inspection_count/input_data_count"] = sum_df["inspection_count"] / sum_df["annotation_count"] return sum_df @@ -638,17 +683,19 @@ def create_member_df(self, task_df: pd.DataFrame) -> pd.DataFrame: member_dict = {} for account_id, member in self.project_members_dict.items(): new_member = copy.deepcopy(member) - new_member.update({ - # 初回のアノテーションに関わった個数(タスクの教師付担当者は変更されない前提) - "task_count_of_first_annotation": 0, - "input_data_count_of_first_annotation": 0, - "annotation_count_of_first_annotation": 0, - "inspection_count_of_first_annotation": 0, - # 関わった作業時間 - "annotation_worktime_hour": 0, - "inspection_worktime_hour": 0, - "acceptance_worktime_hour": 0, - }) + new_member.update( + { + # 初回のアノテーションに関わった個数(タスクの教師付担当者は変更されない前提) + "task_count_of_first_annotation": 0, + "input_data_count_of_first_annotation": 0, + "annotation_count_of_first_annotation": 0, + "inspection_count_of_first_annotation": 0, + # 関わった作業時間 + "annotation_worktime_hour": 0, + "inspection_worktime_hour": 0, + "acceptance_worktime_hour": 0, + } + ) member_dict[account_id] = new_member for _, row_task in task_df.iterrows(): @@ -722,8 +769,9 @@ def create_cumulative_df_by_first_annotator(task_df: pd.DataFrame) -> pd.DataFra task_df: タスク一覧のDataFrame. 列が追加される """ # 教師付の開始時刻でソートして、indexを更新する - df = task_df.sort_values(["first_annotation_account_id", - "first_annotation_started_datetime"]).reset_index(drop=True) + df = task_df.sort_values(["first_annotation_account_id", "first_annotation_started_datetime"]).reset_index( + drop=True + ) # タスクの累計数を取得するために設定する df["task_count"] = 1 # 教師付の作業者でgroupby @@ -756,8 +804,9 @@ def create_cumulative_df_by_first_inspector(task_df: pd.DataFrame) -> pd.DataFra task_df: タスク一覧のDataFrame. 列が追加される """ - df = task_df.sort_values(["first_inspection_account_id", - "first_inspection_started_datetime"]).reset_index(drop=True) + df = task_df.sort_values(["first_inspection_account_id", "first_inspection_started_datetime"]).reset_index( + drop=True + ) # タスクの累計数を取得するために設定する df["task_count"] = 1 # 教師付の作業者でgroupby @@ -790,8 +839,9 @@ def create_cumulative_df_by_first_acceptor(task_df: pd.DataFrame) -> pd.DataFram task_df: タスク一覧のDataFrame. 列が追加される """ - df = task_df.sort_values(["first_acceptance_account_id", - "first_acceptance_started_datetime"]).reset_index(drop=True) + df = task_df.sort_values(["first_acceptance_account_id", "first_acceptance_started_datetime"]).reset_index( + drop=True + ) # タスクの累計数を取得するために設定する df["task_count"] = 1 groupby_obj = df.groupby("first_acceptance_account_id") @@ -844,8 +894,6 @@ def create_worktime_per_image_df(self, aggregation_by: AggregationBy, phase: Tas df = pd.DataFrame(worktime_info_list) # acount_idをusernameに変更する columns = { - col: self._get_username(col) - for col in df.columns - if col != "date" # pylint: disable=not-an-iterable + col: self._get_username(col) for col in df.columns if col != "date" # pylint: disable=not-an-iterable } return df.rename(columns=columns).fillna(0) diff --git a/annofabcli/statistics/tsv.py b/annofabcli/statistics/tsv.py index 85684514..f3c28b4d 100755 --- a/annofabcli/statistics/tsv.py +++ b/annofabcli/statistics/tsv.py @@ -51,10 +51,7 @@ def __init__(self, outdir: str, project_id: str): self.short_project_id = project_id[0:8] def write_inspection_list( - self, - df: pd.DataFrame, - dropped_columns: Optional[List[str]] = None, - only_error_corrected: bool = True, + self, df: pd.DataFrame, dropped_columns: Optional[List[str]] = None, only_error_corrected: bool = True, ): """ 検査コメント一覧をTSVで出力する diff --git a/annofabcli/statistics/visualize_statistics.py b/annofabcli/statistics/visualize_statistics.py index aabf1d5e..91150a4c 100644 --- a/annofabcli/statistics/visualize_statistics.py +++ b/annofabcli/statistics/visualize_statistics.py @@ -35,6 +35,7 @@ class VisualizeStatistics(AbstractCommandLineInterface): """ 統計情報を可視化する。 """ + def visualize_statistics( self, project_id: str, @@ -92,9 +93,7 @@ def visualize_statistics( tsv_obj.write_task_history_list(task_history_df) tsv_obj.write_inspection_list(df=inspection_df, dropped_columns=["data"], only_error_corrected=True) tsv_obj.write_inspection_list( - df=inspection_df_all, - dropped_columns=["data"], - only_error_corrected=False, + df=inspection_df_all, dropped_columns=["data"], only_error_corrected=False, ) tsv_obj.write_member_list(member_df) @@ -118,22 +117,20 @@ def visualize_statistics( graph_obj.write_histogram_for_worktime(task_df) graph_obj.write_histogram_for_other(task_df) graph_obj.write_cumulative_line_graph_for_annotator( - df=task_cumulative_df_by_annotator, - first_annotation_user_id_list=user_id_list, + df=task_cumulative_df_by_annotator, first_annotation_user_id_list=user_id_list, ) graph_obj.write_cumulative_line_graph_for_inspector( - df=task_cumulative_df_by_inspector, - first_inspection_user_id_list=user_id_list, + df=task_cumulative_df_by_inspector, first_inspection_user_id_list=user_id_list, ) graph_obj.write_cumulative_line_graph_for_acceptor( - df=task_cumulative_df_by_acceptor, - first_acceptance_user_id_list=user_id_list, + df=task_cumulative_df_by_acceptor, first_acceptance_user_id_list=user_id_list, ) - graph_obj.write_productivity_line_graph_for_annotator(df=by_date_df, - first_annotation_user_id_list=user_id_list) + graph_obj.write_productivity_line_graph_for_annotator( + df=by_date_df, first_annotation_user_id_list=user_id_list + ) except Exception as e: # pylint: disable=broad-except logger.warning(e) @@ -174,9 +171,11 @@ def parse_args(parser: argparse.ArgumentParser): "-u", "--user_id", nargs="+", - help=("メンバごとの統計グラフに表示するユーザのuser_idを指定してください。" - "指定しない場合は、辞書順に並べた上位20人が表示されます。" - "file://`を先頭に付けると、一覧が記載されたファイルを指定できます。"), + help=( + "メンバごとの統計グラフに表示するユーザのuser_idを指定してください。" + "指定しない場合は、辞書順に並べた上位20人が表示されます。" + "file://`を先頭に付けると、一覧が記載されたファイルを指定できます。" + ), ) parser.add_argument( @@ -191,36 +190,25 @@ def parse_args(parser: argparse.ArgumentParser): parser.add_argument( "--ignored_task_id", nargs="+", - help=("可視化対象外のタスクのtask_id。" - "指定しない場合は、すべてのタスクが可視化対象です。" - "file://`を先頭に付けると、一覧が記載されたファイルを指定できます。"), + help=("可視化対象外のタスクのtask_id。" "指定しない場合は、すべてのタスクが可視化対象です。" "file://`を先頭に付けると、一覧が記載されたファイルを指定できます。"), ) parser.add_argument( - "--not_update", - action="store_true", - help="作業ディレクトリ内のファイルを参照して、統計情報を出力します。" - "AnnoFab Web APIへのアクセスを最小限にします。", + "--not_update", action="store_true", help="作業ディレクトリ内のファイルを参照して、統計情報を出力します。" "AnnoFab Web APIへのアクセスを最小限にします。", ) parser.add_argument( "--update_annotation", action="store_true", - help="アノテーションzipを更新してから、アノテーションzipをダウンロードします。" - "ただし、アノテーションzipの最終更新日時がタスクの最終更新日時より新しい場合は、アノテーションzipを更新しません。", + help="アノテーションzipを更新してから、アノテーションzipをダウンロードします。" "ただし、アノテーションzipの最終更新日時がタスクの最終更新日時より新しい場合は、アノテーションzipを更新しません。", ) parser.add_argument( - "--update_task_json", - action="store_true", - help="タスク全件ファイルJSONを更新してから、タスク全件ファイルJSONをダウンロードします。", + "--update_task_json", action="store_true", help="タスク全件ファイルJSONを更新してから、タスク全件ファイルJSONをダウンロードします。", ) parser.add_argument( - "--work_dir", - type=str, - default=".annofab-cli", - help="作業ディレクトリのパス。指定しない場合カレントの'.annofab-cli'ディレクトリに保存する", + "--work_dir", type=str, default=".annofab-cli", help="作業ディレクトリのパス。指定しない場合カレントの'.annofab-cli'ディレクトリに保存する", ) parser.set_defaults(subcommand_func=main) diff --git a/annofabcli/supplementary/list_supplementary_data.py b/annofabcli/supplementary/list_supplementary_data.py index 1e558cfc..0de6e3a3 100644 --- a/annofabcli/supplementary/list_supplementary_data.py +++ b/annofabcli/supplementary/list_supplementary_data.py @@ -16,13 +16,15 @@ class ListSupplementaryData(AbstractCommandLineInterface): """ 補助情報一覧を表示する。 """ + @annofabcli.utils.allow_404_error def get_supplementary_data_list(self, project_id: str, input_data_id: str) -> SupplementaryData: supplementary_data_list, _ = self.service.api.get_supplementary_data_list(project_id, input_data_id) return supplementary_data_list - def get_all_supplementary_data_list(self, project_id: str, - input_data_id_list: List[str]) -> List[SupplementaryData]: + def get_all_supplementary_data_list( + self, project_id: str, input_data_id_list: List[str] + ) -> List[SupplementaryData]: """ 補助情報一覧を取得する。 """ @@ -68,8 +70,9 @@ def parse_args(parser: argparse.ArgumentParser): argument_parser.add_project_id() argument_parser.add_input_data_id() - argument_parser.add_format(choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON], - default=FormatArgument.CSV) + argument_parser.add_format( + choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON], default=FormatArgument.CSV + ) argument_parser.add_output() argument_parser.add_csv_format() @@ -80,7 +83,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "list" subcommand_help = "補助情報一覧を出力します。" - description = ("補助情報一覧を出力します。") + description = "補助情報一覧を出力します。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description) parse_args(parser) diff --git a/annofabcli/supplementary/subcommand_supplementary.py b/annofabcli/supplementary/subcommand_supplementary.py index af5379db..decbaa60 100644 --- a/annofabcli/supplementary/subcommand_supplementary.py +++ b/annofabcli/supplementary/subcommand_supplementary.py @@ -7,7 +7,7 @@ def parse_args(parser: argparse.ArgumentParser): - subparsers = parser.add_subparsers(dest='subcommand_name') + subparsers = parser.add_subparsers(dest="subcommand_name") # サブコマンドの定義 annofabcli.supplementary.list_supplementary_data.add_parser(subparsers) diff --git a/annofabcli/task/cancel_acceptance.py b/annofabcli/task/cancel_acceptance.py index f3f4089e..a2ed7b9f 100644 --- a/annofabcli/task/cancel_acceptance.py +++ b/annofabcli/task/cancel_acceptance.py @@ -30,8 +30,11 @@ def cancel_acceptance(self, project_id: str, task_id_list: List[str], acceptor_u super().validate_project(project_id, [ProjectMemberRole.OWNER]) - acceptor_account_id = self.facade.get_account_id_from_user_id( - project_id, acceptor_user_id) if acceptor_user_id is not None else None + acceptor_account_id = ( + self.facade.get_account_id_from_user_id(project_id, acceptor_user_id) + if acceptor_user_id is not None + else None + ) logger.info(f"受け入れを取り消すタスク数: {len(task_id_list)}") @@ -79,12 +82,18 @@ def main(args: argparse.Namespace): def parse_args(parser: argparse.ArgumentParser): - parser.add_argument('-p', '--project_id', type=str, required=True, help='対象のプロジェクトのproject_idを指定します。') + parser.add_argument("-p", "--project_id", type=str, required=True, help="対象のプロジェクトのproject_idを指定します。") - parser.add_argument('-t', '--task_id', type=str, required=True, nargs='+', - help='対象のタスクのtask_idを指定します。`file://`を先頭に付けると、task_idの一覧が記載されたファイルを指定できます。') + parser.add_argument( + "-t", + "--task_id", + type=str, + required=True, + nargs="+", + help="対象のタスクのtask_idを指定します。`file://`を先頭に付けると、task_idの一覧が記載されたファイルを指定できます。", + ) - parser.add_argument('-u', '--user_id', type=str, help='再度受入を担当させたいユーザのuser_idを指定します。指定しない場合、タスクの担当者は未割り当てになります。') + parser.add_argument("-u", "--user_id", type=str, help="再度受入を担当させたいユーザのuser_idを指定します。指定しない場合、タスクの担当者は未割り当てになります。") parser.set_defaults(subcommand_func=main) diff --git a/annofabcli/task/change_operator.py b/annofabcli/task/change_operator.py index 8a20cdcb..0e44ce90 100644 --- a/annofabcli/task/change_operator.py +++ b/annofabcli/task/change_operator.py @@ -62,14 +62,17 @@ def change_operator(self, project_id: str, task_id_list: List[str], user_id: Opt else: user_id = None - logger.debug(f"{str_progress} : task_id = {task.task_id}, " - f"status = {task.status.value}, " - f"phase = {task.phase.value}, " - f"user_id = {user_id}") + logger.debug( + f"{str_progress} : task_id = {task.task_id}, " + f"status = {task.status.value}, " + f"phase = {task.phase.value}, " + f"user_id = {user_id}" + ) if task.status in [TaskStatus.COMPLETE, TaskStatus.WORKING]: logger.warning( - f"{str_progress} : task_id = {task_id} : タスクのstatusがworking or complete なので、担当者を変更できません。") + f"{str_progress} : task_id = {task_id} : タスクのstatusがworking or complete なので、担当者を変更できません。" + ) continue if not self.confirm_change_operator(task, user_id): @@ -117,9 +120,9 @@ def parse_args(parser: argparse.ArgumentParser): assign_group = parser.add_mutually_exclusive_group(required=True) - assign_group.add_argument('-u', '--user_id', type=str, help='タスクを新しく担当するユーザのuser_idを指定してください。') + assign_group.add_argument("-u", "--user_id", type=str, help="タスクを新しく担当するユーザのuser_idを指定してください。") - assign_group.add_argument('--not_assign', action="store_true", help='指定した場合、タスクの担当者は未割り当てになります。') + assign_group.add_argument("--not_assign", action="store_true", help="指定した場合、タスクの担当者は未割り当てになります。") parser.set_defaults(subcommand_func=main) @@ -127,7 +130,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "change_operator" subcommand_help = "タスクの担当者を変更します。" - description = ("タスクの担当者を変更します。ただし、作業中のタスクに対しては変更しません。") + description = "タスクの担当者を変更します。ただし、作業中のタスクに対しては変更しません。" epilog = "チェッカーまたはオーナロールを持つユーザで実行してください。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog) diff --git a/annofabcli/task/complete_tasks.py b/annofabcli/task/complete_tasks.py index 0c9827ad..51558bcd 100644 --- a/annofabcli/task/complete_tasks.py +++ b/annofabcli/task/complete_tasks.py @@ -20,6 +20,7 @@ class ComleteTasks(AbstractCommandLineInterface): """ タスクを受け入れ完了にする """ + @staticmethod def inspection_list_to_dict(all_inspection_list: List[Inspection]) -> InspectionJson: """ @@ -28,10 +29,10 @@ def inspection_list_to_dict(all_inspection_list: List[Inspection]) -> Inspection """ task_dict: InspectionJson = {} for inspection in all_inspection_list: - task_id = inspection['task_id'] + task_id = inspection["task_id"] input_data_dict: Dict[InputDataId, List[Inspection]] = task_dict.get(task_id, {}) - input_data_id = inspection['input_data_id'] + input_data_id = inspection["input_data_id"] inspection_list = input_data_dict.get(input_data_id, []) inspection_list.append(inspection) input_data_dict[input_data_id] = inspection_list @@ -40,8 +41,9 @@ def inspection_list_to_dict(all_inspection_list: List[Inspection]) -> Inspection return task_dict - def complete_tasks_with_changing_inspection_status(self, project_id: str, inspection_status: InspectionStatus, - inspection_list: List[Inspection]): + def complete_tasks_with_changing_inspection_status( + self, project_id: str, inspection_status: InspectionStatus, inspection_list: List[Inspection] + ): """ 検査コメントのstatusを変更(対応完了 or 対応不要)にした上で、タスクを受け入れ完了状態にする Args: @@ -91,8 +93,9 @@ def complete_tasks_with_changing_inspection_status(self, project_id: str, inspec # 検査コメントを付与して、タスクを受け入れ完了にする try: - self.complete_acceptance_task(project_id, task, inspection_status, input_data_dict=input_data_dict, - account_id=account_id) + self.complete_acceptance_task( + project_id, task, inspection_status, input_data_dict=input_data_dict, account_id=account_id + ) except requests.HTTPError as e: logger.warning(e) logger.warning(f"{task_id} の受入完了に失敗") @@ -100,8 +103,14 @@ def complete_tasks_with_changing_inspection_status(self, project_id: str, inspec self.facade.change_to_break_phase(project_id, task_id, account_id) continue - def update_status_of_inspections(self, project_id: str, task_id: str, input_data_id: str, - inspection_list: List[Inspection], inspection_status: InspectionStatus): + def update_status_of_inspections( + self, + project_id: str, + task_id: str, + input_data_id: str, + inspection_list: List[Inspection], + inspection_status: InspectionStatus, + ): if inspection_list is None or len(inspection_list) == 0: logger.warning(f"変更対象の検査コメントはなかった。task_id = {task_id}, input_data_id = {input_data_id}") @@ -116,12 +125,19 @@ def filter_inspection(arg_inspection: Inspection) -> bool: return arg_inspection["inspection_id"] in target_inspection_id_list - self.service.wrapper.update_status_of_inspections(project_id, task_id, input_data_id, filter_inspection, - inspection_status) + self.service.wrapper.update_status_of_inspections( + project_id, task_id, input_data_id, filter_inspection, inspection_status + ) logger.debug(f"{task_id}, {input_data_id}, {len(inspection_list)}件 検査コメントの状態を変更") - def complete_acceptance_task(self, project_id: str, task: Task, inspection_status: InspectionStatus, - input_data_dict: Dict[InputDataId, List[Inspection]], account_id: str): + def complete_acceptance_task( + self, + project_id: str, + task: Task, + inspection_status: InspectionStatus, + input_data_dict: Dict[InputDataId, List[Inspection]], + account_id: str, + ): """ 検査コメントのstatusを変更(対応完了 or 対応不要)にした上で、タスクを受け入れ完了状態にする """ @@ -130,8 +146,9 @@ def complete_acceptance_task(self, project_id: str, task: Task, inspection_statu # 検査コメントの状態を変更する for input_data_id, inspection_list in input_data_dict.items(): - self.update_status_of_inspections(project_id, task_id, input_data_id, inspection_list=inspection_list, - inspection_status=inspection_status) + self.update_status_of_inspections( + project_id, task_id, input_data_id, inspection_list=inspection_list, inspection_status=inspection_status + ) # タスクの状態を検査する self.facade.complete_task(project_id, task_id, account_id) @@ -142,8 +159,9 @@ def main(self): inspection_list = annofabcli.common.cli.get_json_from_args(args.inspection_list) inspection_status = InspectionStatus(args.inspection_status) - self.complete_tasks_with_changing_inspection_status(args.project_id, inspection_status=inspection_status, - inspection_list=inspection_list) + self.complete_tasks_with_changing_inspection_status( + args.project_id, inspection_status=inspection_status, inspection_list=inspection_list + ) def parse_args(parser: argparse.ArgumentParser): @@ -152,18 +170,28 @@ def parse_args(parser: argparse.ArgumentParser): argument_parser.add_project_id() parser.add_argument( - '--inspection_list', type=str, required=True, - help=('未処置の検査コメントの一覧をJSON形式で指定します。指定された検査コメントの状態が変更されます。' - '検査コメントの一覧は `annofabcli inspection_comment list_unprocessed` コマンドで出力できます。' - '`file://`を先頭に付けると、JSON形式のファイルを指定できます。' - '実際に参照する値は `task_id`, `input_data_id`, `inspection_id` です。')) + "--inspection_list", + type=str, + required=True, + help=( + "未処置の検査コメントの一覧をJSON形式で指定します。指定された検査コメントの状態が変更されます。" + "検査コメントの一覧は `annofabcli inspection_comment list_unprocessed` コマンドで出力できます。" + "`file://`を先頭に付けると、JSON形式のファイルを指定できます。" + "実際に参照する値は `task_id`, `input_data_id`, `inspection_id` です。" + ), + ) parser.add_argument( - '--inspection_status', type=str, required=True, + "--inspection_status", + type=str, + required=True, choices=[InspectionStatus.ERROR_CORRECTED.value, InspectionStatus.NO_CORRECTION_REQUIRED.value], - help=('未処置の検査コメントをどの状態に変更するかを指定します。' - f'{InspectionStatus.ERROR_CORRECTED.value}: 対応完了,' - f'{InspectionStatus.NO_CORRECTION_REQUIRED.value}: 対応不要')) + help=( + "未処置の検査コメントをどの状態に変更するかを指定します。" + f"{InspectionStatus.ERROR_CORRECTED.value}: 対応完了," + f"{InspectionStatus.NO_CORRECTION_REQUIRED.value}: 対応不要" + ), + ) parser.set_defaults(subcommand_func=main) @@ -177,7 +205,7 @@ def main(args): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "complete" subcommand_help = "未処置の検査コメントを適切な状態に変更して、タスクを受け入れ完了にする。" - description = ("未処置の検査コメントを適切な状態に変更して、タスクを受け入れ完了にする。" "オーナ権限を持つユーザで実行すること。") + description = "未処置の検査コメントを適切な状態に変更して、タスクを受け入れ完了にする。" "オーナ権限を持つユーザで実行すること。" epilog = "チェッカーまたはオーナロールを持つユーザで実行してください。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog) diff --git a/annofabcli/task/delete_tasks.py b/annofabcli/task/delete_tasks.py index 8ddc2d29..94aa53a7 100644 --- a/annofabcli/task/delete_tasks.py +++ b/annofabcli/task/delete_tasks.py @@ -16,13 +16,14 @@ class DeleteTask(AbstractCommandLineInterface): """ アノテーションが付与されていないタスクを削除する """ + @annofabcli.utils.allow_404_error def get_task(self, project_id: str, task_id: str) -> Dict[str, Any]: task, _ = self.service.api.get_task(project_id, task_id) return task def confirm_delete_task(self, task_id: str) -> bool: - message_for_confirm = (f"タスク'{task_id}' を削除しますか?") + message_for_confirm = f"タスク'{task_id}' を削除しますか?" return self.confirm_processing(message_for_confirm) def get_annotation_list(self, project_id: str, task_id: str) -> List[Dict[str, Any]]: @@ -51,8 +52,10 @@ def delete_task(self, project_id: str, task_id: str): logger.info(f"task_id={task_id} のタスクは存在しません。") return False - logger.debug(f"task_id={task['task_id']}, status={task['status']}, " - f"phase={task['phase']}, updated_datetime={task['updated_datetime']}") + logger.debug( + f"task_id={task['task_id']}, status={task['status']}, " + f"phase={task['phase']}, updated_datetime={task['updated_datetime']}" + ) task_status = TaskStatus(task["status"]) if task_status in [TaskStatus.WORKING, TaskStatus.COMPLETE]: @@ -117,7 +120,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "delete" subcommand_help = "タスクを削除します。" - description = ("タスクを削除します。ただしアノテーションが付与されているタスク、作業中/完了状態のタスクは削除できません。") + description = "タスクを削除します。ただしアノテーションが付与されているタスク、作業中/完了状態のタスクは削除できません。" epilog = "オーナロールを持つユーザで実行してください。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog) diff --git a/annofabcli/task/list_tasks.py b/annofabcli/task/list_tasks.py index 1e450ae2..35443750 100644 --- a/annofabcli/task/list_tasks.py +++ b/annofabcli/task/list_tasks.py @@ -22,6 +22,7 @@ class ListTasks(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) @@ -60,27 +61,28 @@ def _modify_task_query(self, project_id: str, task_query: Dict[str, Any]) -> Dic 修正したタスク検索クエリ """ + def remove_key(arg_key: str): if arg_key in task_query: logger.info(f"タスク検索クエリから、`{arg_key}` キーを削除しました。") task_query.pop(arg_key) - remove_key('page') - remove_key('limit') + remove_key("page") + remove_key("limit") - if 'user_id' in task_query: - user_id = task_query['user_id'] + if "user_id" in task_query: + user_id = task_query["user_id"] account_id = self.facade.get_account_id_from_user_id(project_id, user_id) if account_id is not None: - task_query['account_id'] = account_id + task_query["account_id"] = account_id else: logger.warning(f"タスク検索クエリに含まれている user_id: {user_id} のユーザが見つかりませんでした。") - if 'previous_user_id' in task_query: - previous_user_id = task_query['previous_user_id'] + if "previous_user_id" in task_query: + previous_user_id = task_query["previous_user_id"] previous_account_id = self.facade.get_account_id_from_user_id(project_id, previous_user_id) if previous_account_id is not None: - task_query['previous_account_id'] = previous_account_id + task_query["previous_account_id"] = previous_account_id else: logger.warning(f"タスク検索クエリに含まれている previous_user_id: {previous_user_id} のユーザが見つかりませんでした。") @@ -104,8 +106,13 @@ def get_tasks(self, project_id: str, task_query: Optional[Dict[str, Any]] = None tasks = self.service.wrapper.get_all_tasks(project_id, query_params=task_query) return [self.visualize.add_properties_to_task(e) for e in tasks] - def print_tasks(self, project_id: str, task_id_list: Optional[List[str]] = None, - task_query: Optional[Dict[str, Any]] = None, task_list_from_json: Optional[List[Task]] = None): + def print_tasks( + self, + project_id: str, + task_id_list: Optional[List[str]] = None, + task_query: Optional[Dict[str, Any]] = None, + task_list_from_json: Optional[List[Task]] = None, + ): """ タスク一覧を出力する @@ -138,12 +145,15 @@ def print_tasks(self, project_id: str, task_id_list: Optional[List[str]] = None, @staticmethod def validate(args: argparse.Namespace): if args.task_json is not None and args.task_query is not None: - logger.warning("annofabcli task list: warning: argument --task_query: " - "`--task_json`を指定しているときは、`--task_query`オプションは無視します。") + logger.warning( + "annofabcli task list: warning: argument --task_query: " + "`--task_json`を指定しているときは、`--task_query`オプションは無視します。" + ) if args.task_json is not None and args.task_id is not None: - logger.warning("annofabcli task list: warning: argument --task_id: " - "`--task_json`を指定しているときは、`--task_id`オプションは無視します。") + logger.warning( + "annofabcli task list: warning: argument --task_id: " "`--task_json`を指定しているときは、`--task_id`オプションは無視します。" + ) return True @@ -164,8 +174,9 @@ def main(self): else: task_list = None - self.print_tasks(args.project_id, task_id_list=task_id_list, task_query=task_query, - task_list_from_json=task_list) + self.print_tasks( + args.project_id, task_id_list=task_id_list, task_query=task_query, task_list_from_json=task_list + ) def main(args): @@ -183,26 +194,38 @@ def parse_args(parser: argparse.ArgumentParser): # タスク検索クエリ query_group.add_argument( - '-tq', '--task_query', type=str, help='タスクの検索クエリをJSON形式で指定します。指定しない場合は、すべてのタスクを取得します。' - '`file://`を先頭に付けると、JSON形式のファイルを指定できます。' - 'クエリのフォーマットは、[getTasks API](https://annofab.com/docs/api/#operation/getTasks)のクエリパラメータと同じです。' - 'さらに追加で、`user_id`, `previous_user_id` キーも指定できます。' - 'ただし `page`, `limit`キーは指定できません。') + "-tq", + "--task_query", + type=str, + help="タスクの検索クエリをJSON形式で指定します。指定しない場合は、すべてのタスクを取得します。" + "`file://`を先頭に付けると、JSON形式のファイルを指定できます。" + "クエリのフォーマットは、[getTasks API](https://annofab.com/docs/api/#operation/getTasks)のクエリパラメータと同じです。" + "さらに追加で、`user_id`, `previous_user_id` キーも指定できます。" + "ただし `page`, `limit`キーは指定できません。", + ) query_group.add_argument( - '-t', '--task_id', type=str, nargs='+', help='対象のタスクのtask_idを指定します。`--task_query`引数とは同時に指定できません。' - '`file://`を先頭に付けると、task_idの一覧が記載されたファイルを指定できます。') + "-t", + "--task_id", + type=str, + nargs="+", + help="対象のタスクのtask_idを指定します。`--task_query`引数とは同時に指定できません。" "`file://`を先頭に付けると、task_idの一覧が記載されたファイルを指定できます。", + ) parser.add_argument( - '--task_json', type=str, help='タスク情報が記載されたJSONファイルのパスを指定すると、JSONに記載された情報を元にタスク一覧を出力します。' - 'AnnoFabからタスク情報を取得しません。 ' - 'このオプションを指定すると、`--task_query`, `--task_id`オプションは無視します。' - 'JSONには記載されていない、`user_id`や`username`などの情報も追加します。' - 'JSONファイルは`$ annofabcli project download task`コマンドで取得できます。') + "--task_json", + type=str, + help="タスク情報が記載されたJSONファイルのパスを指定すると、JSONに記載された情報を元にタスク一覧を出力します。" + "AnnoFabからタスク情報を取得しません。 " + "このオプションを指定すると、`--task_query`, `--task_id`オプションは無視します。" + "JSONには記載されていない、`user_id`や`username`などの情報も追加します。" + "JSONファイルは`$ annofabcli project download task`コマンドで取得できます。", + ) argument_parser.add_format( choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON, FormatArgument.TASK_ID_LIST], - default=FormatArgument.CSV) + default=FormatArgument.CSV, + ) argument_parser.add_output() argument_parser.add_csv_format() @@ -213,7 +236,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "list" subcommand_help = "タスク一覧を出力します。" - description = ("タスク一覧を出力します。") + description = "タスク一覧を出力します。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description) parse_args(parser) diff --git a/annofabcli/task/reject_tasks.py b/annofabcli/task/reject_tasks.py index 0eefdd7d..67d0230e 100644 --- a/annofabcli/task/reject_tasks.py +++ b/annofabcli/task/reject_tasks.py @@ -22,8 +22,9 @@ class RejectTasks(AbstractCommandLineInterface): - def add_inspection_comment(self, project_id: str, task: Dict[str, Any], inspection_comment: str, - commenter_account_id: str): + def add_inspection_comment( + self, project_id: str, task: Dict[str, Any], inspection_comment: str, commenter_account_id: str + ): """ 先頭画像の左上に検査コメントを付与する Args: @@ -37,31 +38,31 @@ def add_inspection_comment(self, project_id: str, task: Dict[str, Any], inspecti """ first_input_data_id = task["input_data_id_list"][0] - req_inspection = [{ - "data": { - "project_id": project_id, - "comment": inspection_comment, - "task_id": task["task_id"], - "input_data_id": first_input_data_id, - "inspection_id": str(uuid.uuid4()), - "phase": task["phase"], - "commenter_account_id": commenter_account_id, + req_inspection = [ + { "data": { - "x": 0, - "y": 0, - "_type": "Point" + "project_id": project_id, + "comment": inspection_comment, + "task_id": task["task_id"], + "input_data_id": first_input_data_id, + "inspection_id": str(uuid.uuid4()), + "phase": task["phase"], + "commenter_account_id": commenter_account_id, + "data": {"x": 0, "y": 0, "_type": "Point"}, + "status": "annotator_action_required", + "created_datetime": annofabapi.utils.str_now(), }, - "status": "annotator_action_required", - "created_datetime": annofabapi.utils.str_now() - }, - "_type": "Put", - }] + "_type": "Put", + } + ] - return self.service.api.batch_update_inspections(project_id, task["task_id"], first_input_data_id, - request_body=req_inspection)[0] + return self.service.api.batch_update_inspections( + project_id, task["task_id"], first_input_data_id, request_body=req_inspection + )[0] - def confirm_reject_task(self, task_id, assign_last_annotator: bool, - assigned_annotator_user_id: Optional[str]) -> bool: + def confirm_reject_task( + self, task_id, assign_last_annotator: bool, assigned_annotator_user_id: Optional[str] + ) -> bool: confirm_message = f"task_id = {task_id} のタスクを差し戻しますか?" if assign_last_annotator: confirm_message += "最後のannotation phaseの担当者を割り当てます。" @@ -72,9 +73,15 @@ def confirm_reject_task(self, task_id, assign_last_annotator: bool, return self.confirm_processing_task(task_id, confirm_message) - def reject_tasks_with_adding_comment(self, project_id: str, task_id_list: List[str], inspection_comment: str, - commenter_user_id: str, assign_last_annotator: bool = True, - assigned_annotator_user_id: Optional[str] = None): + def reject_tasks_with_adding_comment( + self, + project_id: str, + task_id_list: List[str], + inspection_comment: str, + commenter_user_id: str, + assign_last_annotator: bool = True, + assigned_annotator_user_id: Optional[str] = None, + ): """ 検査コメントを付与して、タスクを差し戻す Args: @@ -94,8 +101,11 @@ def reject_tasks_with_adding_comment(self, project_id: str, task_id_list: List[s logger.error(f"検査コメントを付与するユーザ( {commenter_user_id} の account_idが見つかりませんでした。終了します。") return - assigned_annotator_account_id = self.facade.get_account_id_from_user_id( - project_id, assigned_annotator_user_id) if assigned_annotator_user_id is not None else None + assigned_annotator_account_id = ( + self.facade.get_account_id_from_user_id(project_id, assigned_annotator_user_id) + if assigned_annotator_user_id is not None + else None + ) logger.info(f"差し戻すタスク数: {len(task_id_list)}") success_count = 0 @@ -106,7 +116,8 @@ def reject_tasks_with_adding_comment(self, project_id: str, task_id_list: List[s task, _ = self.service.api.get_task(project_id, task_id) logger.debug( - f"{str_progress} : task_id = {task_id} の現状: status = {task['status']}, phase = {task['phase']}") + f"{str_progress} : task_id = {task_id} の現状: status = {task['status']}, phase = {task['phase']}" + ) if task["phase"] == TaskPhase.ANNOTATION.value: logger.warning(f"{str_progress} : task_id = {task_id} はannofation phaseのため、差し戻しできません。") @@ -119,7 +130,8 @@ def reject_tasks_with_adding_comment(self, project_id: str, task_id_list: List[s # 担当者を変更して、作業中にする self.facade.change_operator_of_task(project_id, task_id, commenter_account_id) logger.debug( - f"{str_progress} : task_id = {task_id}, phase={task['phase']}, {commenter_user_id}に担当者変更 完了") + f"{str_progress} : task_id = {task_id}, phase={task['phase']}, {commenter_user_id}に担当者変更 完了" + ) self.facade.change_to_working_phase(project_id, task_id, commenter_account_id) logger.debug(f"{str_progress} : task_id = {task_id}, phase={task['phase']}, working statusに変更 完了") @@ -146,15 +158,21 @@ def reject_tasks_with_adding_comment(self, project_id: str, task_id_list: List[s if assign_last_annotator: # 最後のannotation phaseに担当を割り当てる _, last_annotator_account_id = self.facade.reject_task_assign_last_annotator( - project_id, task_id, commenter_account_id) + project_id, task_id, commenter_account_id + ) last_annotator_user_id = self.facade.get_user_id_from_account_id( - project_id, last_annotator_account_id) + project_id, last_annotator_account_id + ) str_annotator_user = f"タスクの担当者: {last_annotator_user_id}" else: # 指定したユーザに担当を割り当てる - self.facade.reject_task(project_id, task_id, account_id=commenter_account_id, - annotator_account_id=assigned_annotator_account_id) + self.facade.reject_task( + project_id, + task_id, + account_id=commenter_account_id, + annotator_account_id=assigned_annotator_account_id, + ) str_annotator_user = f"タスクの担当者: {assigned_annotator_user_id}" logger.info(f"{str_progress} : task_id = {task_id} の差し戻し完了. {str_annotator_user}") @@ -173,9 +191,14 @@ def main(self): task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id) user_id = self.service.api.login_user_id assign_last_annotator = not args.not_assign and args.assigned_annotator_user_id is None - self.reject_tasks_with_adding_comment(args.project_id, task_id_list, args.comment, commenter_user_id=user_id, - assign_last_annotator=assign_last_annotator, - assigned_annotator_user_id=args.assigned_annotator_user_id) + self.reject_tasks_with_adding_comment( + args.project_id, + task_id_list, + args.comment, + commenter_user_id=user_id, + assign_last_annotator=assign_last_annotator, + assigned_annotator_user_id=args.assigned_annotator_user_id, + ) def main(args: argparse.Namespace): @@ -190,16 +213,20 @@ def parse_args(parser: argparse.ArgumentParser): argument_parser.add_project_id() argument_parser.add_task_id() - parser.add_argument('-c', '--comment', type=str, required=True, help='差し戻すときに付与する検査コメントを指定します。') + parser.add_argument("-c", "--comment", type=str, required=True, help="差し戻すときに付与する検査コメントを指定します。") # 差し戻したタスクの担当者の割当に関して assign_group = parser.add_mutually_exclusive_group() - assign_group.add_argument('--not_assign', action="store_true", help='差し戻したタスクに担当者を割り当てません。' - '指定しない場合は、最後のannotation phaseの担当者が割り当てられます。') + assign_group.add_argument( + "--not_assign", action="store_true", help="差し戻したタスクに担当者を割り当てません。" "指定しない場合は、最後のannotation phaseの担当者が割り当てられます。" + ) - assign_group.add_argument('--assigned_annotator_user_id', type=str, help='差し戻したタスクに割り当てるユーザのuser_idを指定します。' - '指定しない場合は、最後のannotation phaseの担当者が割り当てられます。') + assign_group.add_argument( + "--assigned_annotator_user_id", + type=str, + help="差し戻したタスクに割り当てるユーザのuser_idを指定します。" "指定しない場合は、最後のannotation phaseの担当者が割り当てられます。", + ) parser.set_defaults(subcommand_func=main) @@ -207,7 +234,7 @@ def parse_args(parser: argparse.ArgumentParser): def add_parser(subparsers: argparse._SubParsersAction): subcommand_name = "reject" subcommand_help = "検査コメントを付与してタスクを差し戻します。" - description = ("検査コメントを付与してタスクを差し戻します。" "検査コメントは、タスク内の先頭の画像の左上(x=0,y=0)に付与します。" "アノテーションルールを途中で変更したときなどに、利用します。") + description = "検査コメントを付与してタスクを差し戻します。" "検査コメントは、タスク内の先頭の画像の左上(x=0,y=0)に付与します。" "アノテーションルールを途中で変更したときなどに、利用します。" epilog = "チェッカーまたはオーナロールを持つユーザで実行してください。" parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog) diff --git a/annofabcli/task/subcommand_task.py b/annofabcli/task/subcommand_task.py index 27207512..f154f731 100644 --- a/annofabcli/task/subcommand_task.py +++ b/annofabcli/task/subcommand_task.py @@ -12,7 +12,7 @@ def parse_args(parser: argparse.ArgumentParser): - subparsers = parser.add_subparsers(dest='subcommand_name') + subparsers = parser.add_subparsers(dest="subcommand_name") # サブコマンドの定義 annofabcli.task.cancel_acceptance.add_parser(subparsers) diff --git a/tests/test_local_cli.py b/tests/test_local_cli.py index 6571d224..cc5c41da 100644 --- a/tests/test_local_cli.py +++ b/tests/test_local_cli.py @@ -6,8 +6,8 @@ # プロジェクトトップに移動する os.chdir(os.path.dirname(os.path.abspath(__file__)) + "/../") -test_dir = Path('./tests/data') -out_dir = Path('./tests/out') +test_dir = Path("./tests/data") +out_dir = Path("./tests/out") def test_get_json_from_args(): diff --git a/tests/test_local_complete_task.py b/tests/test_local_complete_task.py index b0c688fb..aef94722 100644 --- a/tests/test_local_complete_task.py +++ b/tests/test_local_complete_task.py @@ -3,55 +3,21 @@ def test_inspection_list_to_dict(): inspection_list = [ - { - 'task_id': 'task0', - 'input_data_id': 'input0', - 'inspection_id': 'inspection0' - }, - { - 'task_id': 'task1', - 'input_data_id': 'input0', - 'inspection_id': 'inspection1' - }, - { - 'task_id': 'task0', - 'input_data_id': 'input1', - 'inspection_id': 'inspection2' - }, - { - 'task_id': 'task0', - 'input_data_id': 'input0', - 'inspection_id': 'inspection3' - }, + {"task_id": "task0", "input_data_id": "input0", "inspection_id": "inspection0"}, + {"task_id": "task1", "input_data_id": "input0", "inspection_id": "inspection1"}, + {"task_id": "task0", "input_data_id": "input1", "inspection_id": "inspection2"}, + {"task_id": "task0", "input_data_id": "input0", "inspection_id": "inspection3"}, ] expected = { - 'task1': { - 'input0': [{ - 'task_id': 'task1', - 'input_data_id': 'input0', - 'inspection_id': 'inspection1' - }] + "task1": {"input0": [{"task_id": "task1", "input_data_id": "input0", "inspection_id": "inspection1"}]}, + "task0": { + "input1": [{"task_id": "task0", "input_data_id": "input1", "inspection_id": "inspection2"}], + "input0": [ + {"task_id": "task0", "input_data_id": "input0", "inspection_id": "inspection0"}, + {"task_id": "task0", "input_data_id": "input0", "inspection_id": "inspection3"}, + ], }, - 'task0': { - 'input1': [{ - 'task_id': 'task0', - 'input_data_id': 'input1', - 'inspection_id': 'inspection2' - }], - 'input0': [ - { - 'task_id': 'task0', - 'input_data_id': 'input0', - 'inspection_id': 'inspection0' - }, - { - 'task_id': 'task0', - 'input_data_id': 'input0', - 'inspection_id': 'inspection3' - }, - ] - } } assert ComleteTasks.inspection_list_to_dict(inspection_list) == expected diff --git a/tests/test_local_image.py b/tests/test_local_image.py index 77d83a31..2b4a0dd7 100644 --- a/tests/test_local_image.py +++ b/tests/test_local_image.py @@ -10,8 +10,8 @@ # プロジェクトトップに移動する os.chdir(os.path.dirname(os.path.abspath(__file__)) + "/../") -test_dir = Path('./tests/data') -out_dir = Path('./tests/out') +test_dir = Path("./tests/data") +out_dir = Path("./tests/out") with (test_dir / "label_color.json").open(encoding="utf-8") as f: label_color_json = json.load(f) @@ -25,8 +25,13 @@ def test_write_image(): with zipfile.ZipFile(zip_path) as zip_file: parser = SimpleAnnotationZipParser(zip_file, "sample_1/c6e1c2ec-6c7c-41c6-9639-4244c2ed2839.json") - write_annotation_image(parser=parser, image_size=(64, 64), label_color_dict=label_color_dict, - output_image_file=output_image_file, background_color=(64, 64, 64)) + write_annotation_image( + parser=parser, + image_size=(64, 64), + label_color_dict=label_color_dict, + output_image_file=output_image_file, + background_color=(64, 64, 64), + ) def test_write_image_wihtout_outer_file(): @@ -34,8 +39,13 @@ def test_write_image_wihtout_outer_file(): # 外部ファイルが見つからない状態で画像を生成する。 parser = SimpleAnnotationDirParser(test_dir / "simple-annotation.json") - write_annotation_image(parser=parser, image_size=(64, 64), label_color_dict=label_color_dict, - output_image_file=output_image_file, background_color=(64, 64, 64)) + write_annotation_image( + parser=parser, + image_size=(64, 64), + label_color_dict=label_color_dict, + output_image_file=output_image_file, + background_color=(64, 64, 64), + ) def test_write_annotation_images_from_path(): @@ -45,6 +55,11 @@ def test_write_annotation_images_from_path(): def is_target_parser_func(parser: SimpleAnnotationParser) -> bool: return parser.task_id == "sample_1" - write_annotation_images_from_path(annotation_path=zip_path, image_size=(64, 64), label_color_dict=label_color_dict, - output_dir_path=output_image_dir, background_color=(64, 64, 64), - is_target_parser_func=is_target_parser_func) + write_annotation_images_from_path( + annotation_path=zip_path, + image_size=(64, 64), + label_color_dict=label_color_dict, + output_dir_path=output_image_dir, + background_color=(64, 64, 64), + is_target_parser_func=is_target_parser_func, + ) diff --git a/tests/test_local_input_data.py b/tests/test_local_input_data.py index 4be74dda..fb016582 100644 --- a/tests/test_local_input_data.py +++ b/tests/test_local_input_data.py @@ -9,8 +9,8 @@ # プロジェクトトップに移動する os.chdir(os.path.dirname(os.path.abspath(__file__)) + "/../") -test_dir = Path('./tests/data') -out_dir = Path('./tests/out') +test_dir = Path("./tests/data") +out_dir = Path("./tests/out") def is_uuid4(target: str): @@ -25,12 +25,15 @@ def test_get_input_data_list_from_csv(): csv_path = test_dir / "input_data.csv" actual_members = PutInputData.get_input_data_list_from_csv(csv_path) print(actual_members) - assert actual_members[0] == CsvInputData(input_data_name="data1", input_data_path="s3://example.com/data1", - input_data_id="id1", sign_required=None) - assert actual_members[1] == CsvInputData(input_data_name="data2", input_data_path="s3://example.com/data2", - input_data_id="id2", sign_required=True) - assert actual_members[2] == CsvInputData(input_data_name="data3", input_data_path="s3://example.com/data3", - input_data_id="id3", sign_required=False) + assert actual_members[0] == CsvInputData( + input_data_name="data1", input_data_path="s3://example.com/data1", input_data_id="id1", sign_required=None + ) + assert actual_members[1] == CsvInputData( + input_data_name="data2", input_data_path="s3://example.com/data2", input_data_id="id2", sign_required=True + ) + assert actual_members[2] == CsvInputData( + input_data_name="data3", input_data_path="s3://example.com/data3", input_data_id="id3", sign_required=False + ) assert is_uuid4(actual_members[3].input_data_id) @@ -40,7 +43,11 @@ def test_create_datetime_range_list(): last_datetime = datetime.datetime(2019, 1, 4) actual = create_datetime_range_list(first_datetime=first_datetime, last_datetime=last_datetime, days=2) - expected = [(None, datetime.datetime(2019, 1, 1)), (datetime.datetime(2019, 1, 1), datetime.datetime(2019, 1, 3)), - (datetime.datetime(2019, 1, 3), datetime.datetime(2019, 1, 5)), (datetime.datetime(2019, 1, 5), None)] + expected = [ + (None, datetime.datetime(2019, 1, 1)), + (datetime.datetime(2019, 1, 1), datetime.datetime(2019, 1, 3)), + (datetime.datetime(2019, 1, 3), datetime.datetime(2019, 1, 5)), + (datetime.datetime(2019, 1, 5), None), + ] assert actual == expected diff --git a/tests/test_local_project_member.py b/tests/test_local_project_member.py index 1fa3092c..6e9d23f8 100644 --- a/tests/test_local_project_member.py +++ b/tests/test_local_project_member.py @@ -9,8 +9,8 @@ # プロジェクトトップに移動する os.chdir(os.path.dirname(os.path.abspath(__file__)) + "/../") -test_dir = Path('./tests/data') -out_dir = Path('./tests/out') +test_dir = Path("./tests/data") +out_dir = Path("./tests/out") class TestPutProjectMembers: @@ -21,19 +21,24 @@ def test_get_members_from_csv(self): Member("user1", ProjectMemberRole.OWNER, sampling_inspection_rate=None, sampling_acceptance_rate=None), Member("user2", ProjectMemberRole.ACCEPTER, sampling_inspection_rate=10, sampling_acceptance_rate=20), Member("user3", ProjectMemberRole.WORKER, sampling_inspection_rate=None, sampling_acceptance_rate=20), - Member("user4", ProjectMemberRole.TRAINING_DATA_USER, sampling_inspection_rate=10, - sampling_acceptance_rate=None), + Member( + "user4", + ProjectMemberRole.TRAINING_DATA_USER, + sampling_inspection_rate=10, + sampling_acceptance_rate=None, + ), ] assert actual_members == expected_members class TestChangeProjectMembers: def test_validate_member_info(self): - assert ChangeProjectMembers.validate_member_info({ - "sampling_inspection_rate": 10, - "sampling_acceptance_rate": 20, - "foo": "bar" - }) == True + assert ( + ChangeProjectMembers.validate_member_info( + {"sampling_inspection_rate": 10, "sampling_acceptance_rate": 20, "foo": "bar"} + ) + == True + ) assert ChangeProjectMembers.validate_member_info({"sampling_inspection_rate": 10}) == True diff --git a/tests/test_local_statistics.py b/tests/test_local_statistics.py index c42639d6..68d29845 100644 --- a/tests/test_local_statistics.py +++ b/tests/test_local_statistics.py @@ -2,8 +2,8 @@ from annofabcli.statistics.graph import Graph -out_path = Path('./tests/out') -data_path = Path('./tests/data') +out_path = Path("./tests/out") +data_path = Path("./tests/data") project_id = "12345678-abcd-1234-abcd-1234abcd5678" graph_obj = Graph(str(out_path), project_id) diff --git a/tests/test_main.py b/tests/test_main.py index acde6038..6969a50f 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -13,59 +13,83 @@ # プロジェクトトップに移動する os.chdir(os.path.dirname(os.path.abspath(__file__)) + "/../") inifile = configparser.ConfigParser() -inifile.read('./pytest.ini', 'UTF-8') -annofab_config = dict(inifile.items('annofab')) +inifile.read("./pytest.ini", "UTF-8") +annofab_config = dict(inifile.items("annofab")) -project_id = annofab_config['project_id'] -task_id = annofab_config['task_id'] +project_id = annofab_config["project_id"] +task_id = annofab_config["task_id"] service = annofabapi.build_from_netrc() user_id = service.api.login_user_id -out_path = Path('./tests/out') -data_path = Path('./tests/data') +out_path = Path("./tests/out") +data_path = Path("./tests/data") organization_name = service.api.get_organization_of_project(project_id)[0]["organization_name"] class TestAnnotation: def test_annotation(self): - main([ - 'annotation', 'list_count', '--project_id', project_id, '--annotation_query', '{"label_name_en": "car"}', - '--output', - str(out_path / 'annotation_count.csv') - ]) + main( + [ + "annotation", + "list_count", + "--project_id", + project_id, + "--annotation_query", + '{"label_name_en": "car"}', + "--output", + str(out_path / "annotation_count.csv"), + ] + ) class TestAnnotationSpecs: command_name = "annotation_specs" def test_annotation_specs_list_label(self): - out_file = str(out_path / 'anotation_specs_list_label.json') - main([self.command_name, 'list_label', '--project_id', project_id, '--output', out_file]) + out_file = str(out_path / "anotation_specs_list_label.json") + main([self.command_name, "list_label", "--project_id", project_id, "--output", out_file]) def test_old_annotation_specs_list_label(self): - out_file = str(out_path / 'anotation_specs_list_label.json') - main([self.command_name, 'list_label', '--project_id', project_id, '--before', "1", '--output', out_file]) + out_file = str(out_path / "anotation_specs_list_label.json") + main([self.command_name, "list_label", "--project_id", project_id, "--before", "1", "--output", out_file]) def test_annotation_specs_list_label_from_history_id(self): - out_file = str(out_path / 'anotation_specs_list_label.json') + out_file = str(out_path / "anotation_specs_list_label.json") histories, _ = service.api.get_annotation_specs_histories(project_id) - history_id = histories[0]['history_id'] - main([ - self.command_name, 'list_label', '--project_id', project_id, '--history_id', history_id, '--output', - out_file - ]) + history_id = histories[0]["history_id"] + main( + [ + self.command_name, + "list_label", + "--project_id", + project_id, + "--history_id", + history_id, + "--output", + out_file, + ] + ) def test_annotation_specs_list_label_color(self): - out_file = str(out_path / 'anotation_specs_list_label_color.json') - main([ - self.command_name, 'list_label_color', '--project_id', project_id, '--format', 'json', '--output', out_file - ]) + out_file = str(out_path / "anotation_specs_list_label_color.json") + main( + [ + self.command_name, + "list_label_color", + "--project_id", + project_id, + "--format", + "json", + "--output", + out_file, + ] + ) def test_annotation_specs_histories(self): - out_file = str(out_path / 'anotaton_specs_histories.csv') - main([self.command_name, 'history', '--project_id', project_id, '--output', out_file]) + out_file = str(out_path / "anotaton_specs_histories.csv") + main([self.command_name, "history", "--project_id", project_id, "--output", out_file]) class TestFilesystem: @@ -74,122 +98,237 @@ def test_filesystem(self): output_image_dir = out_path / "annotation-image" label_color_file = data_path / "label_color.json" - main([ - 'filesystem', 'write_annotation_image', '--annotation', - str(zip_path), '--output_dir', - str(output_image_dir), '--image_size', '64x64', '--label_color', f"file://{str(label_color_file)}", - '--image_extension', 'jpg' - ]) + main( + [ + "filesystem", + "write_annotation_image", + "--annotation", + str(zip_path), + "--output_dir", + str(output_image_dir), + "--image_size", + "64x64", + "--label_color", + f"file://{str(label_color_file)}", + "--image_extension", + "jpg", + ] + ) class TestInputData: command_name = "input_data" def test_delete_input_data(self): - main(['input_data', 'delete', '--project_id', project_id, '--input_data_id', 'foo', '--yes']) + main(["input_data", "delete", "--project_id", project_id, "--input_data_id", "foo", "--yes"]) def test_list_input_data(self): - out_file = str(out_path / 'input_data.json') - main([ - 'input_data', 'list', '--project_id', project_id, '--input_data_query', '{"input_data_name": "abcdefg"}', - '--add_details', '--output', out_file - ]) + out_file = str(out_path / "input_data.json") + main( + [ + "input_data", + "list", + "--project_id", + project_id, + "--input_data_query", + '{"input_data_name": "abcdefg"}', + "--add_details", + "--output", + out_file, + ] + ) def test_list_input_data_with_batch(self): - out_file = str(out_path / 'input_data.json') - main([ - 'input_data', 'list', '--project_id', project_id, '--input_data_query', '{"input_data_name": "abcdefg"}', - '--batch', '{"first":"2019-01-01", "last":"2019-01-31", "days":7}', '--add_details', '--output', out_file - ]) + out_file = str(out_path / "input_data.json") + main( + [ + "input_data", + "list", + "--project_id", + project_id, + "--input_data_query", + '{"input_data_name": "abcdefg"}', + "--batch", + '{"first":"2019-01-01", "last":"2019-01-31", "days":7}', + "--add_details", + "--output", + out_file, + ] + ) def test_put_input_data(self): csv_file = str(data_path / "input_data2.csv") - main([self.command_name, 'put', '--project_id', project_id, '--csv', csv_file, '--overwrite', '--yes']) + main([self.command_name, "put", "--project_id", project_id, "--csv", csv_file, "--overwrite", "--yes"]) def test_put_input_data_with_zip(self): # 注意:ジョブ登録される zip_file = str(data_path / "lenna.zip") - main([ - self.command_name, 'put', '--project_id', project_id, '--zip', zip_file, '--wait', '--yes', - '--wait_options', '{"interval":1, "max_tries":1}' - ]) + main( + [ + self.command_name, + "put", + "--project_id", + project_id, + "--zip", + zip_file, + "--wait", + "--yes", + "--wait_options", + '{"interval":1, "max_tries":1}', + ] + ) class TestInspectionComment: def test_list_inspection_comment(self): - out_file = str(out_path / 'inspection_comment.csv') - main(['inspection_comment', 'list', '--project_id', project_id, '--task_id', task_id, '--output', out_file]) + out_file = str(out_path / "inspection_comment.csv") + main(["inspection_comment", "list", "--project_id", project_id, "--task_id", task_id, "--output", out_file]) def test_list_inspection_comment_from_json(self): - out_file = str(out_path / 'inspection_comment.csv') + out_file = str(out_path / "inspection_comment.csv") inspection_comment_json = str(data_path / "inspection-comment.json") - main([ - 'inspection_comment', 'list', '--project_id', project_id, '--inspection_comment_json', - inspection_comment_json, '--output', out_file - ]) + main( + [ + "inspection_comment", + "list", + "--project_id", + project_id, + "--inspection_comment_json", + inspection_comment_json, + "--output", + out_file, + ] + ) def test_list_unprocessed_inspection_comment(self): - out_file = str(out_path / 'inspection_comment.csv') - main([ - 'inspection_comment', 'list_unprocessed', '--project_id', project_id, '--task_id', task_id, '--output', - out_file - ]) + out_file = str(out_path / "inspection_comment.csv") + main( + [ + "inspection_comment", + "list_unprocessed", + "--project_id", + project_id, + "--task_id", + task_id, + "--output", + out_file, + ] + ) def test_list_unprocessed_inspection_comment_from_json(self): - out_file = str(out_path / 'inspection_comment.csv') + out_file = str(out_path / "inspection_comment.csv") inspection_comment_json = str(data_path / "inspection-comment.json") - main([ - 'inspection_comment', 'list_unprocessed', '--project_id', project_id, '--inspection_comment_json', - inspection_comment_json, '--output', out_file - ]) + main( + [ + "inspection_comment", + "list_unprocessed", + "--project_id", + project_id, + "--inspection_comment_json", + inspection_comment_json, + "--output", + out_file, + ] + ) class TestInstruction: def test_upload_instruction(self): - html_file = str(data_path / 'instruction.html') - main(['instruction', 'upload', '--project_id', project_id, '--html', html_file]) + html_file = str(data_path / "instruction.html") + main(["instruction", "upload", "--project_id", project_id, "--html", html_file]) class TestJob: def test_list_job(self): - out_file = str(out_path / 'job.csv') - main([ - 'job', 'list', '--project_id', project_id, '--job_type', "gen-annotation", '--format', 'csv', '--output', - out_file - ]) + out_file = str(out_path / "job.csv") + main( + [ + "job", + "list", + "--project_id", + project_id, + "--job_type", + "gen-annotation", + "--format", + "csv", + "--output", + out_file, + ] + ) def test_list_last_job(self): - out_file = str(out_path / 'job.csv') - main([ - 'job', 'list_last', '--project_id', project_id, '--job_type', "gen-annotation", '--format', 'csv', - '--output', out_file - ]) + out_file = str(out_path / "job.csv") + main( + [ + "job", + "list_last", + "--project_id", + project_id, + "--job_type", + "gen-annotation", + "--format", + "csv", + "--output", + out_file, + ] + ) class TestLabor: def test_list_worktime_by_user_with_project_id(self): - output_dir = str(out_path / 'labor') - main([ - 'labor', 'list_worktime_by_user', '--project_id', project_id, '--user_id', service.api.login_user_id, - '--start_date', '2019-09-01', '--end_date', '2019-09-01', '--output_dir', - str(output_dir) - ]) + output_dir = str(out_path / "labor") + main( + [ + "labor", + "list_worktime_by_user", + "--project_id", + project_id, + "--user_id", + service.api.login_user_id, + "--start_date", + "2019-09-01", + "--end_date", + "2019-09-01", + "--output_dir", + str(output_dir), + ] + ) def test_list_worktime_by_user_with_organization_name(self): - output_dir = str(out_path / 'labor') - main([ - 'labor', 'list_worktime_by_user', '--organization', organization_name, '--user_id', - service.api.login_user_id, '--start_date', '2019-09-01', '--end_date', '2019-09-01', '--output_dir', - str(output_dir) - ]) + output_dir = str(out_path / "labor") + main( + [ + "labor", + "list_worktime_by_user", + "--organization", + organization_name, + "--user_id", + service.api.login_user_id, + "--start_date", + "2019-09-01", + "--end_date", + "2019-09-01", + "--output_dir", + str(output_dir), + ] + ) class TestOrganizationMember: def test_list_organization_member(self): - out_file = str(out_path / 'organization_member.csv') - main([ - 'organization_member', 'list', '--organization', organization_name, '--format', 'csv', '--output', out_file - ]) + out_file = str(out_path / "organization_member.csv") + main( + [ + "organization_member", + "list", + "--organization", + organization_name, + "--format", + "csv", + "--output", + out_file, + ] + ) class TestProject: @@ -198,108 +337,171 @@ class TestProject: # main(['project', 'copy', '--project_id', 'not_exists_project_id', '--dest_title', 'copy-project']) def test_diff_project(self): - main(['project', 'diff', project_id, project_id]) + main(["project", "diff", project_id, project_id]) def test_download_project_task(self): - out_file = str(out_path / 'task.json') - main(['project', 'download', 'task', '--project_id', project_id, '--output', out_file]) + out_file = str(out_path / "task.json") + main(["project", "download", "task", "--project_id", project_id, "--output", out_file]) def test_download_project_inspection_comment(self): - out_file = str(out_path / 'inspection_comment.json') - main(['project', 'download', 'inspection_comment', '--project_id', project_id, '--output', out_file]) + out_file = str(out_path / "inspection_comment.json") + main(["project", "download", "inspection_comment", "--project_id", project_id, "--output", out_file]) def test_download_project_task_history_event(self): - out_file = str(out_path / 'task_history_event.json') - main(['project', 'download', 'task_history_event', '--project_id', project_id, '--output', out_file]) + out_file = str(out_path / "task_history_event.json") + main(["project", "download", "task_history_event", "--project_id", project_id, "--output", out_file]) def test_download_project_simple_annotation(self): - out_file = str(out_path / 'simple_annotation.zip') - main(['project', 'download', 'simple_annotation', '--project_id', project_id, '--output', out_file]) + out_file = str(out_path / "simple_annotation.zip") + main(["project", "download", "simple_annotation", "--project_id", project_id, "--output", out_file]) def test_download_project_full_annotation(self): - out_file = str(out_path / 'full_annotation.zip') - main(['project', 'download', 'full_annotation', '--project_id', project_id, '--output', out_file]) + out_file = str(out_path / "full_annotation.zip") + main(["project", "download", "full_annotation", "--project_id", project_id, "--output", out_file]) def test_list_project(self): - out_file = str(out_path / 'project.csv') - main([ - 'project', 'list', '--organization', organization_name, '--project_query', '{"status": "active"}', - '--format', 'csv', '--output', out_file - ]) + out_file = str(out_path / "project.csv") + main( + [ + "project", + "list", + "--organization", + organization_name, + "--project_query", + '{"status": "active"}', + "--format", + "csv", + "--output", + out_file, + ] + ) class TestProjectMember: def test_put_project_member(self): csv_file = str(data_path / "project_members.csv") - main(['project_member', 'put', '--project_id', project_id, '--csv', csv_file, '--yes']) + main(["project_member", "put", "--project_id", project_id, "--csv", csv_file, "--yes"]) def test_list_project_member(self): - main(['project_member', 'list', '--project_id', project_id]) - main(['project_member', 'list', '--organization', organization_name]) + main(["project_member", "list", "--project_id", project_id]) + main(["project_member", "list", "--organization", organization_name]) def test_copy_project_member(self): - main(['project_member', 'copy', project_id, project_id, '--yes']) + main(["project_member", "copy", project_id, project_id, "--yes"]) def test_invite_project_member(self): - main(['project_member', 'invite', '--user_id', user_id, '--role', 'owner', '--project_id', project_id]) + main(["project_member", "invite", "--user_id", user_id, "--role", "owner", "--project_id", project_id]) def test_change_project_member(self): - main([ - 'project_member', 'change', '--all_user', '--project_id', project_id, '--member_info', - '{"sampling_inspection_rate": 10, "sampling_acceptance_rate": 20}', '--yes' - ]) + main( + [ + "project_member", + "change", + "--all_user", + "--project_id", + project_id, + "--member_info", + '{"sampling_inspection_rate": 10, "sampling_acceptance_rate": 20}', + "--yes", + ] + ) class TestSupplementary: def test_list_project_member(self): - main(['supplementary', 'list', '--project_id', project_id, '--input_data_id', 'foo']) + main(["supplementary", "list", "--project_id", project_id, "--input_data_id", "foo"]) class TestTask: command_name = "task" def test_list(self): - out_file = str(out_path / 'task.csv') - main([ - self.command_name, 'list', '--project_id', project_id, '--task_query', - f'{{"user_id": "{user_id}", "phase":"acceptance", "status": "complete"}}', '--output', out_file, '--format', - 'csv' - ]) + out_file = str(out_path / "task.csv") + main( + [ + self.command_name, + "list", + "--project_id", + project_id, + "--task_query", + f'{{"user_id": "{user_id}", "phase":"acceptance", "status": "complete"}}', + "--output", + out_file, + "--format", + "csv", + ] + ) def test_list_with_task_json(self): - out_file = str(out_path / 'task.csv') + out_file = str(out_path / "task.csv") task_json = str(data_path / "task.json") - main([ - self.command_name, 'list', '--project_id', project_id, '--task_json', task_json, '--output', out_file, - '--format', 'csv' - ]) + main( + [ + self.command_name, + "list", + "--project_id", + project_id, + "--task_json", + task_json, + "--output", + out_file, + "--format", + "csv", + ] + ) def test_cancel_acceptance(self): - main([self.command_name, 'cancel_acceptance', '--project_id', project_id, '--task_id', task_id, '--yes']) + main([self.command_name, "cancel_acceptance", "--project_id", project_id, "--task_id", task_id, "--yes"]) def test_delete_task(self): - main([self.command_name, 'delete', '--project_id', project_id, '--task_id', "not-exists-task", '--yes']) + main([self.command_name, "delete", "--project_id", project_id, "--task_id", "not-exists-task", "--yes"]) def test_reject_task(self): inspection_comment = datetime.datetime.now().isoformat() - main([ - self.command_name, 'reject', '--project_id', project_id, '--task_id', task_id, '--comment', - inspection_comment, '--yes' - ]) + main( + [ + self.command_name, + "reject", + "--project_id", + project_id, + "--task_id", + task_id, + "--comment", + inspection_comment, + "--yes", + ] + ) def test_change_operator(self): # user指定 - main([ - self.command_name, 'change_operator', '--project_id', project_id, '--task_id', task_id, '--user_id', - user_id, '--yes' - ]) + main( + [ + self.command_name, + "change_operator", + "--project_id", + project_id, + "--task_id", + task_id, + "--user_id", + user_id, + "--yes", + ] + ) # 未割り当て - main([ - self.command_name, 'change_operator', '--project_id', project_id, '--task_id', task_id, '--not_assign', - '--yes' - ]) + main( + [ + self.command_name, + "change_operator", + "--project_id", + project_id, + "--task_id", + task_id, + "--not_assign", + "--yes", + ] + ) # def test_complete_task(self): # main([ From fef722e3d9758a24fb26dbb042a2becd3d53b134 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Fri, 27 Dec 2019 18:40:02 +0900 Subject: [PATCH 15/19] =?UTF-8?q?[lint]flake8,=20mypy=E3=81=AB=E5=AF=BE?= =?UTF-8?q?=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../list_annotation_specs_label.py | 4 ++-- annofabcli/input_data/put_input_data.py | 2 +- annofabcli/labor/list_worktime_by_user.py | 18 +++++++++++++++--- annofabcli/statistics/graph.py | 2 +- setup.cfg | 6 ++++-- 5 files changed, 23 insertions(+), 9 deletions(-) diff --git a/annofabcli/annotation_specs/list_annotation_specs_label.py b/annofabcli/annotation_specs/list_annotation_specs_label.py index 74492cef..be09546c 100644 --- a/annofabcli/annotation_specs/list_annotation_specs_label.py +++ b/annofabcli/annotation_specs/list_annotation_specs_label.py @@ -56,7 +56,7 @@ def _print_text_format_labels(labels, output: Optional[str] = None): for label in labels: output_lines.append( "\t".join( - [label["label_id"], label["annotation_type"],] + [label["label_id"], label["annotation_type"]] + PrintAnnotationSpecsLabel._get_name_list(label["label_name"]["messages"]) ) ) @@ -75,7 +75,7 @@ def _print_text_format_labels(labels, output: Optional[str] = None): for choice in additional_data_definition["choices"]: output_lines.append( "\t".join( - ["", "", choice["choice_id"], "",] + ["", "", choice["choice_id"], ""] + PrintAnnotationSpecsLabel._get_name_list(choice["name"]["messages"]) ) ) diff --git a/annofabcli/input_data/put_input_data.py b/annofabcli/input_data/put_input_data.py index 5c2cac2a..5bbf66bf 100644 --- a/annofabcli/input_data/put_input_data.py +++ b/annofabcli/input_data/put_input_data.py @@ -54,7 +54,7 @@ def put_input_data( file_path = get_file_scheme_path(csv_input_data.input_data_path) if file_path is not None: request_body.update( - {"input_data_name": csv_input_data.input_data_name, "sign_required": csv_input_data.sign_required,} + {"input_data_name": csv_input_data.input_data_name, "sign_required": csv_input_data.sign_required} ) logger.debug(f"'{file_path}'を入力データとして登録します。") self.service.wrapper.put_input_data_from_file( diff --git a/annofabcli/labor/list_worktime_by_user.py b/annofabcli/labor/list_worktime_by_user.py index 88718f42..e038b4b0 100644 --- a/annofabcli/labor/list_worktime_by_user.py +++ b/annofabcli/labor/list_worktime_by_user.py @@ -331,7 +331,7 @@ def print_labor_worktime_list( @staticmethod def validate(args: argparse.Namespace) -> bool: if args.organization is not None and args.user_id is None: - print("ERROR: argument --user_id: " "`--organization`を指定しているときは、`--user_id`オプションは必須です。", file=sys.stderr) + print("ERROR: argument --user_id: `--organization`を指定しているときは、`--user_id`オプションは必須です。", file=sys.stderr) return False date_period_is_valid = args.start_date is not None and args.end_date is not None @@ -348,6 +348,15 @@ def validate(args: argparse.Namespace) -> bool: return True def get_user_id_list_from_project_id_list(self, project_id_list: List[str]) -> List[str]: + """ + プロジェクトメンバ一覧からuser_id_listを取得する。 + Args: + project_id_list: + + Returns: + user_id_list + + """ member_list: List[Dict[str, Any]] = [] for project_id in project_id_list: member_list.extend(self.service.wrapper.get_all_project_members(project_id)) @@ -406,12 +415,15 @@ def main(self) -> None: if not self.validate(args): return - user_id_list = get_list_from_args(args.user_id) if args.user_id is not None else None + arg_user_id_list = get_list_from_args(args.user_id) if args.user_id is not None else None project_id_list = get_list_from_args(args.project_id) if args.project_id is not None else None organization_name_list = get_list_from_args(args.organization) if args.organization is not None else None - if user_id_list is None and project_id_list is not None: + if arg_user_id_list is None: + assert project_id_list is not None, "arg_user_id_list is Noneのときは、`project_id_list is not None`であることを期待します。" user_id_list = self.get_user_id_list_from_project_id_list(project_id_list) + else: + user_id_list = arg_user_id_list start_date, end_date = self.get_start_and_end_date_from_args(args) diff --git a/annofabcli/statistics/graph.py b/annofabcli/statistics/graph.py index 058eaa2a..4b5bca24 100644 --- a/annofabcli/statistics/graph.py +++ b/annofabcli/statistics/graph.py @@ -66,7 +66,7 @@ def _create_hover_tool(tool_tip_items: List[str] = None) -> HoverTool: detail_tooltips = [(e, "@{%s}" % e) for e in tool_tip_items] - hover_tool = HoverTool(tooltips=[("index", "$index"), ("(x,y)", "($x, $y)"),] + detail_tooltips) + hover_tool = HoverTool(tooltips=[("index", "$index"), ("(x,y)", "($x, $y)")] + detail_tooltips) return hover_tool @staticmethod diff --git a/setup.cfg b/setup.cfg index 94eea0fb..ed236820 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,8 +10,10 @@ split_before_named_assigns = False [flake8] max-line-length = 120 -# F401: 'xxxx' imported but unused -ignore = F401, E124, W503 + +# formatter 'black'と競合するため、チェックを無視する +# https://black.readthedocs.io/en/stable/the_black_code_style.html#line-length +ignore = E203, W503, F401 [mypy] ignore_missing_imports = True From b907a9bb95ff6744eee161b72f025cf3827e8969 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Fri, 27 Dec 2019 18:48:13 +0900 Subject: [PATCH 16/19] =?UTF-8?q?autoflake8=E3=82=92=E6=9C=89=E5=8A=B9?= =?UTF-8?q?=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 6 +++--- annofabcli/job/list_job.py | 2 +- setup.cfg | 7 +++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index f6a2d757..bed1ea40 100644 --- a/Makefile +++ b/Makefile @@ -12,10 +12,10 @@ init: pipenv install --dev format: + pipenv run autoflake --in-place --remove-all-unused-imports --ignore-init-module-imports --recursive ${FORMAT_FILES} pipenv run black ${FORMAT_FILES} -# pipenv run autoflake --in-place --remove-all-unused-imports --ignore-init-module-imports --recursive ${FORMAT_FILES} -# pipenv run isort --verbose --recursive ${FORMAT_FILES} - #pipenv run yapf --verbose --in-place --recursive ${FORMAT_FILES} + pipenv run isort --verbose --recursive ${FORMAT_FILES} + lint: pipenv run mypy ${LINT_FILES} --config-file setup.cfg diff --git a/annofabcli/job/list_job.py b/annofabcli/job/list_job.py index a562866a..900f2fbb 100644 --- a/annofabcli/job/list_job.py +++ b/annofabcli/job/list_job.py @@ -1,7 +1,7 @@ import argparse import copy import logging -from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import +from typing import Any, Dict, List, Optional from annofabapi.models import JobInfo, JobType diff --git a/setup.cfg b/setup.cfg index ed236820..916e30bb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,6 +21,13 @@ ignore_missing_imports = True [isort] line_length = 120 skip=annofabcli/__init__.py +# blackの設定に合わせる +# https://black.readthedocs.io/en/stable/the_black_code_style.html#line-length +multi_line_output=3 +include_trailing_comma=True +force_grid_wrap=0 +use_parentheses=True + [pylint] disable = From 0b3fae4c4e38e002ee982db7d4d074f365e82119 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Fri, 27 Dec 2019 18:52:59 +0900 Subject: [PATCH 17/19] =?UTF-8?q?=E4=B8=8D=E8=A6=81=E3=81=AAimport?= =?UTF-8?q?=E3=82=92=E5=89=8A=E9=99=A4=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- annofabcli/__main__.py | 2 +- annofabcli/annotation/list_annotation_count.py | 2 +- annofabcli/annotation_specs/list_annotation_specs_history.py | 2 +- annofabcli/annotation_specs/list_annotation_specs_label.py | 2 +- annofabcli/annotation_specs/print_label_color.py | 2 +- annofabcli/common/cli.py | 2 +- annofabcli/common/exceptions.py | 2 +- annofabcli/common/facade.py | 2 +- annofabcli/common/image.py | 2 +- annofabcli/common/typing.py | 2 +- annofabcli/common/utils.py | 2 +- annofabcli/common/visualize.py | 2 +- annofabcli/deprecated_register_annotation_from_full_zip.py | 2 +- annofabcli/experimental/list_labor_worktime.py | 2 +- annofabcli/experimental/utils.py | 2 +- annofabcli/filesystem/write_annotation_image.py | 2 +- annofabcli/input_data/delete_input_data.py | 2 +- annofabcli/input_data/list_input_data.py | 2 +- annofabcli/input_data/put_input_data.py | 2 +- annofabcli/inspection_comment/list_inspections.py | 2 +- annofabcli/inspection_comment/list_unprocessed_inspections.py | 2 +- annofabcli/instruction/upload_instruction.py | 1 - annofabcli/job/list_last_job.py | 2 +- annofabcli/labor/list_worktime_by_user.py | 2 +- annofabcli/organization_member/list_organization_member.py | 1 - annofabcli/project/copy_project.py | 2 +- annofabcli/project/diff_projects.py | 2 +- annofabcli/project/download.py | 1 - annofabcli/project/list_project.py | 2 +- annofabcli/project_member/change_project_members.py | 2 +- annofabcli/project_member/copy_project_members.py | 2 +- annofabcli/project_member/delete_users.py | 2 +- annofabcli/project_member/invite_users.py | 2 +- annofabcli/project_member/list_users.py | 2 +- annofabcli/project_member/put_project_members.py | 2 +- annofabcli/statistics/graph.py | 2 +- annofabcli/statistics/visualize_statistics.py | 2 +- annofabcli/supplementary/list_supplementary_data.py | 2 +- annofabcli/task/cancel_acceptance.py | 2 +- annofabcli/task/change_operator.py | 2 +- annofabcli/task/complete_tasks.py | 2 +- annofabcli/task/delete_tasks.py | 2 +- annofabcli/task/list_tasks.py | 2 +- annofabcli/task/reject_tasks.py | 2 +- 44 files changed, 41 insertions(+), 44 deletions(-) diff --git a/annofabcli/__main__.py b/annofabcli/__main__.py index 9d0a645d..72614547 100644 --- a/annofabcli/__main__.py +++ b/annofabcli/__main__.py @@ -1,6 +1,6 @@ import argparse import logging -from typing import Optional, Sequence # pylint: disable=unused-import +from typing import Optional, Sequence import annofabcli.annotation.subcommand_annotation import annofabcli.annotation_specs.subcommand_annotation_specs diff --git a/annofabcli/annotation/list_annotation_count.py b/annofabcli/annotation/list_annotation_count.py index abbc9925..cbf8bf01 100644 --- a/annofabcli/annotation/list_annotation_count.py +++ b/annofabcli/annotation/list_annotation_count.py @@ -4,7 +4,7 @@ import argparse import logging from enum import Enum -from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import +from typing import Any, Dict, List, Optional import annofabapi import more_itertools diff --git a/annofabcli/annotation_specs/list_annotation_specs_history.py b/annofabcli/annotation_specs/list_annotation_specs_history.py index da8ccc70..596af68e 100644 --- a/annofabcli/annotation_specs/list_annotation_specs_history.py +++ b/annofabcli/annotation_specs/list_annotation_specs_history.py @@ -1,6 +1,6 @@ import argparse import logging -from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import +from typing import Any, Dict, List import annofabapi diff --git a/annofabcli/annotation_specs/list_annotation_specs_label.py b/annofabcli/annotation_specs/list_annotation_specs_label.py index be09546c..32501b31 100644 --- a/annofabcli/annotation_specs/list_annotation_specs_label.py +++ b/annofabcli/annotation_specs/list_annotation_specs_label.py @@ -5,7 +5,7 @@ import argparse import logging import sys -from typing import Any, Callable, Dict, List, Optional, Tuple # pylint: disable=unused-import +from typing import Any, Dict, List, Optional from annofabapi.models import ProjectMemberRole diff --git a/annofabcli/annotation_specs/print_label_color.py b/annofabcli/annotation_specs/print_label_color.py index ff4e96d3..771850f2 100644 --- a/annofabcli/annotation_specs/print_label_color.py +++ b/annofabcli/annotation_specs/print_label_color.py @@ -4,7 +4,7 @@ import argparse import logging -from typing import Any, Callable, Dict, List, Optional, Tuple # pylint: disable=unused-import +from typing import Any, Dict, Tuple from annofabapi.models import ProjectMemberRole diff --git a/annofabcli/common/cli.py b/annofabcli/common/cli.py index 60fad080..967dbe54 100644 --- a/annofabcli/common/cli.py +++ b/annofabcli/common/cli.py @@ -7,7 +7,7 @@ import getpass import json import logging -from typing import Any, Dict, List, Optional, Tuple # pylint: disable=unused-import +from typing import Any, Dict, List, Optional, Tuple import annofabapi import jmespath diff --git a/annofabcli/common/exceptions.py b/annofabcli/common/exceptions.py index 6ba17f73..5b7803d6 100644 --- a/annofabcli/common/exceptions.py +++ b/annofabcli/common/exceptions.py @@ -4,7 +4,7 @@ This module contains the set of annofabapi exceptions. """ -from typing import List, Optional # pylint: disable=unused-import +from typing import List from annofabapi.models import OrganizationMemberRole, ProjectMemberRole diff --git a/annofabcli/common/facade.py b/annofabcli/common/facade.py index 0eab0dd2..5ae2077f 100644 --- a/annofabcli/common/facade.py +++ b/annofabcli/common/facade.py @@ -3,7 +3,7 @@ """ import logging -from typing import Any, Callable, Dict, List, NewType, Optional, Tuple # pylint: disable=unused-import +from typing import Any, Callable, Dict, List, Optional, Tuple import annofabapi import annofabapi.utils diff --git a/annofabcli/common/image.py b/annofabcli/common/image.py index d5d8cb1b..5373ef9b 100644 --- a/annofabcli/common/image.py +++ b/annofabcli/common/image.py @@ -5,7 +5,7 @@ import logging import zipfile from pathlib import Path -from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple # pylint: disable=unused-import +from typing import Any, Callable, Dict, Optional import PIL import PIL.Image diff --git a/annofabcli/common/typing.py b/annofabcli/common/typing.py index 3ad90081..2c72c076 100644 --- a/annofabcli/common/typing.py +++ b/annofabcli/common/typing.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import +from typing import List, Tuple # 型タイプ RGB = Tuple[int, int, int] diff --git a/annofabcli/common/utils.py b/annofabcli/common/utils.py index 6213adbb..50c6f7eb 100644 --- a/annofabcli/common/utils.py +++ b/annofabcli/common/utils.py @@ -5,7 +5,7 @@ import re import sys from pathlib import Path -from typing import Any, Dict, List, Optional, Set, TypeVar # pylint: disable=unused-import +from typing import Any, Dict, List, Optional, Set, TypeVar import dateutil.parser import isodate diff --git a/annofabcli/common/visualize.py b/annofabcli/common/visualize.py index 59198061..e1489c35 100644 --- a/annofabcli/common/visualize.py +++ b/annofabcli/common/visualize.py @@ -3,7 +3,7 @@ """ import enum -from typing import Any, Callable, Dict, List, Optional, Tuple # pylint: disable=unused-import +from typing import Any, Dict, List, Optional import annofabapi import more_itertools diff --git a/annofabcli/deprecated_register_annotation_from_full_zip.py b/annofabcli/deprecated_register_annotation_from_full_zip.py index 66751358..c266ca8b 100644 --- a/annofabcli/deprecated_register_annotation_from_full_zip.py +++ b/annofabcli/deprecated_register_annotation_from_full_zip.py @@ -8,7 +8,7 @@ import logging import uuid from pathlib import Path -from typing import Any, Callable, Dict, List, Optional # pylint: disable=unused-import +from typing import Any, Callable, Dict, List import annofabapi import PIL diff --git a/annofabcli/experimental/list_labor_worktime.py b/annofabcli/experimental/list_labor_worktime.py index 6993f2b3..e929272e 100644 --- a/annofabcli/experimental/list_labor_worktime.py +++ b/annofabcli/experimental/list_labor_worktime.py @@ -4,7 +4,7 @@ import argparse import datetime import logging -from typing import Any, Dict, List, Tuple # pylint: disable=unused-import +from typing import Any, Dict, List, Tuple import annofabapi from annofabapi.models import ProjectMemberRole diff --git a/annofabcli/experimental/utils.py b/annofabcli/experimental/utils.py index c982fe20..33e19a71 100644 --- a/annofabcli/experimental/utils.py +++ b/annofabcli/experimental/utils.py @@ -2,7 +2,7 @@ # type: ignore # pylint: skip-file from datetime import date, timedelta -from typing import Any, Dict # pylint: disable=unused-import +from typing import Any, Dict def date_range(start_date: date, end_date: date): diff --git a/annofabcli/filesystem/write_annotation_image.py b/annofabcli/filesystem/write_annotation_image.py index 70b39424..ac3ad108 100644 --- a/annofabcli/filesystem/write_annotation_image.py +++ b/annofabcli/filesystem/write_annotation_image.py @@ -6,7 +6,7 @@ import logging import zipfile from pathlib import Path -from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple # pylint: disable=unused-import +from typing import List, Optional from annofabapi.models import TaskStatus from annofabapi.parser import SimpleAnnotationParser diff --git a/annofabcli/input_data/delete_input_data.py b/annofabcli/input_data/delete_input_data.py index 509ae720..7c8d3bee 100644 --- a/annofabcli/input_data/delete_input_data.py +++ b/annofabcli/input_data/delete_input_data.py @@ -1,6 +1,6 @@ import argparse import logging -from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import +from typing import Any, Dict, List import requests from annofabapi.models import ProjectMemberRole diff --git a/annofabcli/input_data/list_input_data.py b/annofabcli/input_data/list_input_data.py index f34f312c..37d53d50 100644 --- a/annofabcli/input_data/list_input_data.py +++ b/annofabcli/input_data/list_input_data.py @@ -4,7 +4,7 @@ import logging import urllib.parse from dataclasses import dataclass -from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import +from typing import Any, Dict, List, Optional, Tuple import annofabapi from annofabapi.models import InputData, Task diff --git a/annofabcli/input_data/put_input_data.py b/annofabcli/input_data/put_input_data.py index 5bbf66bf..33d1da7e 100644 --- a/annofabcli/input_data/put_input_data.py +++ b/annofabcli/input_data/put_input_data.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from distutils.util import strtobool # pylint: disable=import-error,no-name-in-module from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import +from typing import Any, Dict, List, Optional import pandas import requests diff --git a/annofabcli/inspection_comment/list_inspections.py b/annofabcli/inspection_comment/list_inspections.py index 2c1aa351..9ec180df 100644 --- a/annofabcli/inspection_comment/list_inspections.py +++ b/annofabcli/inspection_comment/list_inspections.py @@ -6,7 +6,7 @@ import json import logging import sys -from typing import Any, Callable, Dict, List, Optional # pylint: disable=unused-import +from typing import Callable, List, Optional import annofabapi import requests diff --git a/annofabcli/inspection_comment/list_unprocessed_inspections.py b/annofabcli/inspection_comment/list_unprocessed_inspections.py index c7d094a6..da9cd2da 100644 --- a/annofabcli/inspection_comment/list_unprocessed_inspections.py +++ b/annofabcli/inspection_comment/list_unprocessed_inspections.py @@ -2,7 +2,7 @@ import json import logging import sys -from typing import Any, Callable, Dict, List, Optional # pylint: disable=unused-import +from typing import Callable from annofabapi.models import Inspection diff --git a/annofabcli/instruction/upload_instruction.py b/annofabcli/instruction/upload_instruction.py index 4fecdbb2..61fac23e 100644 --- a/annofabcli/instruction/upload_instruction.py +++ b/annofabcli/instruction/upload_instruction.py @@ -3,7 +3,6 @@ import time import uuid from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import import pyquery diff --git a/annofabcli/job/list_last_job.py b/annofabcli/job/list_last_job.py index 21713a63..40af462c 100644 --- a/annofabcli/job/list_last_job.py +++ b/annofabcli/job/list_last_job.py @@ -1,7 +1,7 @@ import argparse import logging import sys -from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import +from typing import Any, Dict, List, Optional from annofabapi.models import JobInfo, JobType, Project diff --git a/annofabcli/labor/list_worktime_by_user.py b/annofabcli/labor/list_worktime_by_user.py index e038b4b0..b3ab1cb4 100644 --- a/annofabcli/labor/list_worktime_by_user.py +++ b/annofabcli/labor/list_worktime_by_user.py @@ -5,7 +5,7 @@ import sys from dataclasses import dataclass from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import +from typing import Any, Dict, List, Optional, Tuple import more_itertools import pandas diff --git a/annofabcli/organization_member/list_organization_member.py b/annofabcli/organization_member/list_organization_member.py index e75291a0..d0701a27 100644 --- a/annofabcli/organization_member/list_organization_member.py +++ b/annofabcli/organization_member/list_organization_member.py @@ -1,6 +1,5 @@ import argparse import logging -from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import import annofabcli from annofabcli import AnnofabApiFacade diff --git a/annofabcli/project/copy_project.py b/annofabcli/project/copy_project.py index 25c91ff2..2c634418 100644 --- a/annofabcli/project/copy_project.py +++ b/annofabcli/project/copy_project.py @@ -2,7 +2,7 @@ import copy import logging import uuid -from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import +from typing import Any, Dict, Optional from annofabapi.models import JobType, OrganizationMemberRole, ProjectMemberRole diff --git a/annofabcli/project/diff_projects.py b/annofabcli/project/diff_projects.py index 80040b31..af155a35 100644 --- a/annofabcli/project/diff_projects.py +++ b/annofabcli/project/diff_projects.py @@ -8,7 +8,7 @@ import logging import pprint from enum import Enum -from typing import Any, Dict, List, Tuple # pylint: disable=unused-import +from typing import Any, Dict, List, Tuple import annofabapi import dictdiffer diff --git a/annofabcli/project/download.py b/annofabcli/project/download.py index d72e45d4..caaa2699 100644 --- a/annofabcli/project/download.py +++ b/annofabcli/project/download.py @@ -1,7 +1,6 @@ import argparse import logging from enum import Enum -from typing import Any, Dict, List, Optional # pylint: disable=unused-import from annofabapi.models import JobStatus, JobType diff --git a/annofabcli/project/list_project.py b/annofabcli/project/list_project.py index e78a0f2f..ca714ad0 100644 --- a/annofabcli/project/list_project.py +++ b/annofabcli/project/list_project.py @@ -1,6 +1,6 @@ import argparse import logging -from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import +from typing import Any, Dict, List, Optional from annofabapi.models import OrganizationMember, Project from more_itertools import first_true diff --git a/annofabcli/project_member/change_project_members.py b/annofabcli/project_member/change_project_members.py index 4942903b..0b38ee84 100644 --- a/annofabcli/project_member/change_project_members.py +++ b/annofabcli/project_member/change_project_members.py @@ -1,7 +1,7 @@ import argparse import logging import sys -from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import +from typing import Any, Dict, List, Optional import requests from annofabapi.models import ProjectMember, ProjectMemberRole, ProjectMemberStatus diff --git a/annofabcli/project_member/copy_project_members.py b/annofabcli/project_member/copy_project_members.py index 2648c90d..9e6113ce 100644 --- a/annofabcli/project_member/copy_project_members.py +++ b/annofabcli/project_member/copy_project_members.py @@ -1,6 +1,6 @@ import argparse import logging -from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import +from typing import Any, Dict, List, Optional import annofabapi from annofabapi.models import OrganizationMember, ProjectMember, ProjectMemberRole diff --git a/annofabcli/project_member/delete_users.py b/annofabcli/project_member/delete_users.py index e57d6681..2bf6f4da 100644 --- a/annofabcli/project_member/delete_users.py +++ b/annofabcli/project_member/delete_users.py @@ -3,7 +3,7 @@ """ import argparse import logging -from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import +from typing import List import requests diff --git a/annofabcli/project_member/invite_users.py b/annofabcli/project_member/invite_users.py index aaeb9d52..652ab68d 100644 --- a/annofabcli/project_member/invite_users.py +++ b/annofabcli/project_member/invite_users.py @@ -4,7 +4,7 @@ import argparse import logging -from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import +from typing import List import requests from annofabapi.models import ProjectMemberRole diff --git a/annofabcli/project_member/list_users.py b/annofabcli/project_member/list_users.py index 87b77144..83c570c4 100644 --- a/annofabcli/project_member/list_users.py +++ b/annofabcli/project_member/list_users.py @@ -3,7 +3,7 @@ """ import argparse import logging -from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import +from typing import Any, Dict, List import more_itertools import requests diff --git a/annofabcli/project_member/put_project_members.py b/annofabcli/project_member/put_project_members.py index f4ed134d..5535940d 100644 --- a/annofabcli/project_member/put_project_members.py +++ b/annofabcli/project_member/put_project_members.py @@ -2,7 +2,7 @@ import logging from dataclasses import dataclass from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import +from typing import Any, Dict, List, Optional import more_itertools import pandas diff --git a/annofabcli/statistics/graph.py b/annofabcli/statistics/graph.py index 4b5bca24..ec1a0875 100644 --- a/annofabcli/statistics/graph.py +++ b/annofabcli/statistics/graph.py @@ -2,7 +2,7 @@ import logging from dataclasses import dataclass from pathlib import Path -from typing import Any, Dict, List, Optional # pylint: disable=unused-import +from typing import Dict, List, Optional import bokeh import bokeh.layouts diff --git a/annofabcli/statistics/visualize_statistics.py b/annofabcli/statistics/visualize_statistics.py index 91150a4c..f15d842e 100644 --- a/annofabcli/statistics/visualize_statistics.py +++ b/annofabcli/statistics/visualize_statistics.py @@ -2,7 +2,7 @@ import json import logging.handlers from pathlib import Path -from typing import Any, Callable, Dict, List, Optional # pylint: disable=unused-import +from typing import Any, Dict, List import annofabapi from annofabapi.models import ProjectMemberRole, TaskPhase diff --git a/annofabcli/supplementary/list_supplementary_data.py b/annofabcli/supplementary/list_supplementary_data.py index 0de6e3a3..8f76dadd 100644 --- a/annofabcli/supplementary/list_supplementary_data.py +++ b/annofabcli/supplementary/list_supplementary_data.py @@ -1,6 +1,6 @@ import argparse import logging -from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import +from typing import List from annofabapi.models import SupplementaryData diff --git a/annofabcli/task/cancel_acceptance.py b/annofabcli/task/cancel_acceptance.py index a2ed7b9f..2d0af86c 100644 --- a/annofabcli/task/cancel_acceptance.py +++ b/annofabcli/task/cancel_acceptance.py @@ -4,7 +4,7 @@ import argparse import logging -from typing import List, Optional # pylint: disable=unused-import +from typing import List, Optional import requests from annofabapi.models import ProjectMemberRole diff --git a/annofabcli/task/change_operator.py b/annofabcli/task/change_operator.py index 0e44ce90..f23f67fb 100644 --- a/annofabcli/task/change_operator.py +++ b/annofabcli/task/change_operator.py @@ -4,7 +4,7 @@ import argparse import logging -from typing import Any, Dict, List, Optional # pylint: disable=unused-import +from typing import List, Optional import requests from annofabapi.dataclass.task import Task diff --git a/annofabcli/task/complete_tasks.py b/annofabcli/task/complete_tasks.py index 51558bcd..8682ec54 100644 --- a/annofabcli/task/complete_tasks.py +++ b/annofabcli/task/complete_tasks.py @@ -1,7 +1,7 @@ import argparse import logging import time -from typing import Any, Callable, Dict, List, Optional # pylint: disable=unused-import +from typing import Dict, List import requests from annofabapi.models import InputDataId, Inspection, InspectionStatus, ProjectMemberRole, Task, TaskId, TaskPhase diff --git a/annofabcli/task/delete_tasks.py b/annofabcli/task/delete_tasks.py index 94aa53a7..3d25ec3a 100644 --- a/annofabcli/task/delete_tasks.py +++ b/annofabcli/task/delete_tasks.py @@ -1,6 +1,6 @@ import argparse import logging -from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import +from typing import Any, Dict, List import requests from annofabapi.models import ProjectMemberRole, TaskStatus diff --git a/annofabcli/task/list_tasks.py b/annofabcli/task/list_tasks.py index 35443750..f5e3e9a1 100644 --- a/annofabcli/task/list_tasks.py +++ b/annofabcli/task/list_tasks.py @@ -4,7 +4,7 @@ import argparse import json import logging -from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import +from typing import Any, Dict, List, Optional import annofabapi from annofabapi.models import Task diff --git a/annofabcli/task/reject_tasks.py b/annofabcli/task/reject_tasks.py index 67d0230e..c49064bf 100644 --- a/annofabcli/task/reject_tasks.py +++ b/annofabcli/task/reject_tasks.py @@ -6,7 +6,7 @@ import logging import time import uuid -from typing import Any, Dict, List, Optional # pylint: disable=unused-import +from typing import Any, Dict, List, Optional import annofabapi import annofabapi.utils From 68f2b2e13a1a9eb681928dff0ad8486f3bd1ac99 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Fri, 27 Dec 2019 18:55:13 +0900 Subject: [PATCH 18/19] =?UTF-8?q?flake8=E3=81=AE=E8=A8=AD=E5=AE=9A?= =?UTF-8?q?=E3=82=92=E8=A6=8B=E7=9B=B4=E3=81=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- annofabcli/__init__.py | 1 + setup.cfg | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/annofabcli/__init__.py b/annofabcli/__init__.py index 6c662e3e..be38c529 100644 --- a/annofabcli/__init__.py +++ b/annofabcli/__init__.py @@ -1,3 +1,4 @@ +# flake8: noqa: F401 from annofabcli.common import enums from annofabcli.common import typing from annofabcli.common import exceptions diff --git a/setup.cfg b/setup.cfg index 916e30bb..07c2129c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,7 @@ max-line-length = 120 # formatter 'black'と競合するため、チェックを無視する # https://black.readthedocs.io/en/stable/the_black_code_style.html#line-length -ignore = E203, W503, F401 +ignore = E203, W503 [mypy] ignore_missing_imports = True From b51599df3bbe86c09a68e4ba46c484ff25b3ac9b Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Fri, 27 Dec 2019 19:37:30 +0900 Subject: [PATCH 19/19] version up --- annofabcli/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/annofabcli/__version__.py b/annofabcli/__version__.py index 8c590a56..5ec839f4 100644 --- a/annofabcli/__version__.py +++ b/annofabcli/__version__.py @@ -1 +1 @@ -__version__ = "1.15.9" +__version__ = "1.16.10"