annofabcli 1.113.0__py3-none-any.whl → 1.114.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,447 @@
1
+ import argparse
2
+ import json
3
+ import logging
4
+ import tempfile
5
+ from enum import Enum
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import isodate
10
+ import pandas
11
+ from annofabapi.models import ProjectMemberRole, Task, TaskPhase, TaskStatus
12
+ from annofabapi.resource import Resource as AnnofabResource
13
+ from annofabapi.utils import get_number_of_rejections
14
+
15
+ import annofabcli.common
16
+ from annofabcli.common.cli import (
17
+ ArgumentParser,
18
+ CommandLine,
19
+ build_annofabapi_resource_and_login,
20
+ )
21
+ from annofabcli.common.download import DownloadingFile
22
+ from annofabcli.common.facade import AnnofabApiFacade
23
+ from annofabcli.common.type_util import assert_noreturn
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ def isoduration_to_second(duration: str) -> float:
29
+ """
30
+ ISO 8601 duration を 秒に変換する
31
+ """
32
+ return isodate.parse_duration(duration).total_seconds()
33
+
34
+
35
+ class TaskStatusForSummary(Enum):
36
+ """
37
+ Annofabユーザーがタスクについて知りたい粒度でまとめて分解した、タスクの状態。
38
+ """
39
+
40
+ NEVER_WORKED_UNASSIGNED = "never_worked.unassigned"
41
+ """一度も作業していない状態かつ担当者未割り当て"""
42
+ NEVER_WORKED_ASSIGNED = "never_worked.assigned"
43
+ """一度も作業していない状態かつ担当者割り当て済み"""
44
+ WORKED_NOT_REJECTED = "worked.not_rejected"
45
+ """タスクのstatusは作業中または休憩中 AND 次のフェーズでまだ差し戻されていない(次のフェーズに進んでいない)"""
46
+ WORKED_REJECTED = "worked.rejected"
47
+ """タスクのstatusは作業中または休憩中 AND 次のフェーズで差し戻された"""
48
+ ON_HOLD = "on_hold"
49
+ """保留中"""
50
+ COMPLETE = "complete"
51
+ """完了"""
52
+
53
+ @staticmethod
54
+ def _get_not_started_status(task: Task, task_history_list: list[dict[str, Any]], not_worked_threshold_second: float) -> "TaskStatusForSummary":
55
+ """
56
+ NOT_STARTED状態のタスクについて、一度も作業されていないか、差し戻されて未着手かを判定する。
57
+
58
+ Args:
59
+ task: タスク情報
60
+ task_history_list: タスク履歴のリスト
61
+ not_worked_threshold_second: 作業していないとみなす作業時間の閾値(秒)。この値以下なら作業していないとみなす。
62
+ """
63
+ # `number_of_inspections=1`を指定する理由:多段検査を無視して、検査フェーズが1回目かどうかを知りたいため
64
+ step = get_step_for_current_phase(task, number_of_inspections=1)
65
+ if step == 1:
66
+ phase = task["phase"]
67
+ worktime_second = sum(isoduration_to_second(history["accumulated_labor_time_milliseconds"]) for history in task_history_list if history["phase"] == phase)
68
+ if worktime_second <= not_worked_threshold_second:
69
+ # 一度も作業されていない(または閾値以下の作業時間)
70
+ account_id = task["account_id"]
71
+ if account_id is None:
72
+ return TaskStatusForSummary.NEVER_WORKED_UNASSIGNED
73
+ else:
74
+ return TaskStatusForSummary.NEVER_WORKED_ASSIGNED
75
+ else:
76
+ return TaskStatusForSummary.WORKED_NOT_REJECTED
77
+ else:
78
+ return TaskStatusForSummary.WORKED_NOT_REJECTED
79
+
80
+ @staticmethod
81
+ def _get_working_or_break_status(task: Task) -> "TaskStatusForSummary":
82
+ """
83
+ WORKINGまたはBREAK状態のタスクについて、rejectされているかどうかを判定する。
84
+ """
85
+ # `number_of_inspections=1`を指定する理由:多段検査を無視して、検査フェーズが1回目かどうかを知りたいため
86
+ step = get_step_for_current_phase(task, number_of_inspections=1)
87
+ if step == 1:
88
+ return TaskStatusForSummary.WORKED_NOT_REJECTED
89
+ else:
90
+ return TaskStatusForSummary.WORKED_REJECTED
91
+
92
+ @staticmethod
93
+ def from_task(task: dict[str, Any], task_history_list: list[dict[str, Any]], not_worked_threshold_second: float = 0) -> "TaskStatusForSummary":
94
+ """
95
+ タスク情報(dict)からインスタンスを生成します。
96
+
97
+ Args:
98
+ task: APIから取得したタスク情報. 以下のkeyが必要です。
99
+ * status
100
+ * phase
101
+ * phase_stage
102
+ * histories_by_phase
103
+ * account_id
104
+ task_history_list: タスク履歴のリスト
105
+ not_worked_threshold_second: 作業していないとみなす作業時間の閾値(秒)。この値以下なら作業していないとみなす。
106
+
107
+ """
108
+ status = TaskStatus(task["status"])
109
+ match status:
110
+ case TaskStatus.COMPLETE:
111
+ return TaskStatusForSummary.COMPLETE
112
+
113
+ case TaskStatus.ON_HOLD:
114
+ return TaskStatusForSummary.ON_HOLD
115
+
116
+ case TaskStatus.NOT_STARTED:
117
+ return TaskStatusForSummary._get_not_started_status(task, task_history_list, not_worked_threshold_second)
118
+
119
+ case TaskStatus.BREAK | TaskStatus.WORKING:
120
+ return TaskStatusForSummary._get_working_or_break_status(task)
121
+
122
+ case _:
123
+ raise RuntimeError(f"'{status}'は対象外です。")
124
+
125
+
126
+ def get_step_for_current_phase(task: Task, number_of_inspections: int) -> int:
127
+ """
128
+ 今のフェーズが何回目かを取得する。
129
+
130
+ Args:
131
+ task: 対象タスクの情報。phase, phase_stage, histories_by_phase を含む。
132
+ number_of_inspections: 対象プロジェクトの検査フェーズの回数
133
+
134
+ Returns:
135
+ 現在のフェーズのステップ数
136
+ """
137
+ current_phase = TaskPhase(task["phase"])
138
+ current_phase_stage = task["phase_stage"]
139
+ histories_by_phase = task["histories_by_phase"]
140
+
141
+ number_of_rejections_by_acceptance = get_number_of_rejections(histories_by_phase, phase=TaskPhase.ACCEPTANCE)
142
+ match current_phase:
143
+ case TaskPhase.ACCEPTANCE:
144
+ return number_of_rejections_by_acceptance + 1
145
+
146
+ case TaskPhase.ANNOTATION:
147
+ number_of_rejections_by_inspection = sum(
148
+ get_number_of_rejections(histories_by_phase, phase=TaskPhase.INSPECTION, phase_stage=phase_stage) for phase_stage in range(1, number_of_inspections + 1)
149
+ )
150
+ return number_of_rejections_by_inspection + number_of_rejections_by_acceptance + 1
151
+
152
+ case TaskPhase.INSPECTION:
153
+ number_of_rejections_by_inspection = sum(
154
+ get_number_of_rejections(histories_by_phase, phase=TaskPhase.INSPECTION, phase_stage=phase_stage) for phase_stage in range(current_phase_stage, number_of_inspections + 1)
155
+ )
156
+ return number_of_rejections_by_inspection + number_of_rejections_by_acceptance + 1
157
+
158
+ case _ as unreachable:
159
+ assert_noreturn(unreachable)
160
+
161
+
162
+ def create_df_task(
163
+ task_list: list[dict[str, Any]], task_history_dict: dict[str, list[dict[str, Any]]], not_worked_threshold_second: float = 0, metadata_keys: list[str] | None = None
164
+ ) -> pandas.DataFrame:
165
+ """
166
+ 以下の列が含まれたタスクのDataFrameを生成します。
167
+ * task_id
168
+ * phase
169
+ * task_status_for_summary
170
+ * metadata.{key} (metadata_keysで指定された各キー)
171
+
172
+ Args:
173
+ task_list: タスク情報のlist
174
+ task_history_dict: タスクIDをキーとしたタスク履歴のdict
175
+ not_worked_threshold_second: 作業していないとみなす作業時間の閾値(秒)
176
+ metadata_keys: 集計対象のメタデータキーのリスト
177
+
178
+ Returns:
179
+ タスク情報のDataFrame
180
+ """
181
+ metadata_keys = metadata_keys or []
182
+
183
+ for task in task_list:
184
+ task_history = task_history_dict[task["task_id"]]
185
+ task["task_status_for_summary"] = TaskStatusForSummary.from_task(task, task_history, not_worked_threshold_second).value
186
+
187
+ # メタデータの値を抽出
188
+ metadata = task.get("metadata", {})
189
+ for key in metadata_keys:
190
+ task[f"metadata.{key}"] = metadata.get(key, "")
191
+
192
+ columns = ["task_id", "phase"] + [f"metadata.{key}" for key in metadata_keys] + ["task_status_for_summary"]
193
+ df = pandas.DataFrame(task_list, columns=columns)
194
+ return df
195
+
196
+
197
+ def aggregate_df(df: pandas.DataFrame, metadata_keys: list[str] | None = None) -> pandas.DataFrame:
198
+ """
199
+ タスク数を集計する。
200
+
201
+ Args:
202
+ df: 以下の列を持つDataFrame
203
+ * phase
204
+ * task_status_for_summary
205
+ * metadata.{key} (metadata_keysで指定された各キー)
206
+ metadata_keys: 集計対象のメタデータキーのリスト
207
+
208
+ Returns:
209
+ indexがphase(とmetadata.*列),列がtask_status_for_summaryであるDataFrame
210
+ """
211
+ metadata_keys = metadata_keys or []
212
+ metadata_columns = [f"metadata.{key}" for key in metadata_keys]
213
+
214
+ df["task_count"] = 1
215
+ index_columns = ["phase", *metadata_columns]
216
+ df2 = df.pivot_table(values="task_count", index=index_columns, columns="task_status_for_summary", aggfunc="sum", fill_value=0)
217
+
218
+ # 列数を固定する
219
+ for status in TaskStatusForSummary:
220
+ if status.value not in df2.columns:
221
+ df2[status.value] = 0
222
+
223
+ # ソート処理
224
+ sorted_phase = [
225
+ TaskPhase.ANNOTATION.value,
226
+ TaskPhase.INSPECTION.value,
227
+ TaskPhase.ACCEPTANCE.value,
228
+ ]
229
+
230
+ if len(metadata_columns) > 0:
231
+ # メタデータ列がある場合は、phase列でソートし、その後メタデータ列でソート
232
+ df2 = df2.reset_index()
233
+ df2["_phase_order"] = df2["phase"].map(lambda x: sorted_phase.index(x) if x in sorted_phase else len(sorted_phase))
234
+ df2 = df2.sort_values(["_phase_order", *metadata_columns])
235
+ df2 = df2.drop(columns=["_phase_order"])
236
+ else:
237
+ # メタデータ列がない場合は既存のロジック
238
+ new_index = sorted(df2.index, key=lambda x: sorted_phase.index(x) if x in sorted_phase else len(sorted_phase))
239
+ df2 = df2.loc[new_index]
240
+ df2 = df2.reset_index()
241
+
242
+ # 列の順序を設定
243
+ result_columns = [
244
+ "phase",
245
+ *metadata_columns,
246
+ TaskStatusForSummary.NEVER_WORKED_UNASSIGNED.value,
247
+ TaskStatusForSummary.NEVER_WORKED_ASSIGNED.value,
248
+ TaskStatusForSummary.WORKED_NOT_REJECTED.value,
249
+ TaskStatusForSummary.WORKED_REJECTED.value,
250
+ TaskStatusForSummary.ON_HOLD.value,
251
+ TaskStatusForSummary.COMPLETE.value,
252
+ ]
253
+ df2 = df2[result_columns]
254
+ return df2
255
+
256
+
257
+ class GettingTaskCountSummary:
258
+ """
259
+ タスク数のサマリーを取得するクラス
260
+ """
261
+
262
+ def __init__(
263
+ self,
264
+ annofab_service: AnnofabResource,
265
+ project_id: str,
266
+ *,
267
+ temp_dir: Path | None = None,
268
+ should_execute_get_tasks_api: bool = False,
269
+ not_worked_threshold_second: float = 0,
270
+ metadata_keys: list[str] | None = None,
271
+ ) -> None:
272
+ self.annofab_service = annofab_service
273
+ self.project_id = project_id
274
+ self.temp_dir = temp_dir
275
+ self.should_execute_get_tasks_api = should_execute_get_tasks_api
276
+ self.not_worked_threshold_second = not_worked_threshold_second
277
+ self.metadata_keys = metadata_keys or []
278
+
279
+ def create_df_task(self) -> pandas.DataFrame:
280
+ """
281
+ 以下の列が含まれたタスクのDataFrameを生成します。
282
+ * task_id
283
+ * phase
284
+ * task_status_for_summary
285
+ * metadata.{key} (metadata_keys で指定した各メタデータキーに対応する列)
286
+ """
287
+ if self.should_execute_get_tasks_api:
288
+ task_list = self.annofab_service.wrapper.get_all_tasks(self.project_id)
289
+ task_history_dict = {}
290
+ for index, task in enumerate(task_list, start=1):
291
+ task_id = task["task_id"]
292
+ task_history_dict[task_id], _ = self.annofab_service.api.get_task_histories(self.project_id, task_id)
293
+ if index % 100 == 0:
294
+ logger.info(f"{index} 件目のタスク履歴を取得しました。")
295
+
296
+ else:
297
+ task_list = self.get_task_list_with_downloading()
298
+ task_history_dict = self.get_task_history_with_downloading()
299
+
300
+ df = create_df_task(task_list, task_history_dict, self.not_worked_threshold_second, self.metadata_keys)
301
+ return df
302
+
303
+ def _get_task_list_with_downloading(self, temp_dir: Path) -> list[dict[str, Any]]:
304
+ downloading_obj = DownloadingFile(self.annofab_service)
305
+ task_json = downloading_obj.download_task_json_to_dir(self.project_id, temp_dir)
306
+ with task_json.open(encoding="utf-8") as f:
307
+ return json.load(f)
308
+
309
+ def _get_task_history_with_downloading(self, temp_dir: Path) -> dict[str, list[dict[str, Any]]]:
310
+ downloading_obj = DownloadingFile(self.annofab_service)
311
+ task_history_json = downloading_obj.download_task_history_json_to_dir(self.project_id, temp_dir)
312
+ with task_history_json.open(encoding="utf-8") as f:
313
+ return json.load(f)
314
+
315
+ def get_task_list_with_downloading(self) -> list[dict[str, Any]]:
316
+ """
317
+ タスク全件ファイルをダウンロードしてタスク情報を取得する。
318
+ """
319
+ if self.temp_dir is not None:
320
+ return self._get_task_list_with_downloading(self.temp_dir)
321
+ else:
322
+ with tempfile.TemporaryDirectory() as str_temp_dir:
323
+ return self._get_task_list_with_downloading(Path(str_temp_dir))
324
+
325
+ def get_task_history_with_downloading(self) -> dict[str, list[dict[str, Any]]]:
326
+ """
327
+ タスク履歴全件ファイルをダウンロードしてタスク情報を取得する。
328
+ """
329
+ if self.temp_dir is not None:
330
+ return self._get_task_history_with_downloading(self.temp_dir)
331
+ else:
332
+ with tempfile.TemporaryDirectory() as str_temp_dir:
333
+ return self._get_task_history_with_downloading(Path(str_temp_dir))
334
+
335
+
336
+ class ListTaskCountByPhase(CommandLine):
337
+ """
338
+ フェーズごとのタスク数を一覧表示する。
339
+ """
340
+
341
+ def list_task_count_by_phase(
342
+ self, project_id: str, *, temp_dir: Path | None = None, should_execute_get_tasks_api: bool = False, not_worked_threshold_second: float = 0, metadata_keys: list[str] | None = None
343
+ ) -> None:
344
+ """
345
+ フェーズごとのタスク数をCSV形式で出力する。
346
+
347
+ Args:
348
+ project_id: プロジェクトID
349
+ temp_dir: 一時ファイルの保存先ディレクトリ
350
+ should_execute_get_tasks_api: getTasks APIを実行するかどうか
351
+ not_worked_threshold_second: 作業していないとみなす作業時間の閾値(秒)
352
+ metadata_keys: 集計対象のメタデータキーのリスト
353
+ """
354
+ super().validate_project(project_id, project_member_roles=[ProjectMemberRole.OWNER, ProjectMemberRole.TRAINING_DATA_USER])
355
+
356
+ logger.info(f"project_id='{project_id}' :: フェーズごとのタスク数を集計します。")
357
+
358
+ getting_obj = GettingTaskCountSummary(
359
+ self.service, project_id, temp_dir=temp_dir, should_execute_get_tasks_api=should_execute_get_tasks_api, not_worked_threshold_second=not_worked_threshold_second, metadata_keys=metadata_keys
360
+ )
361
+ df_task = getting_obj.create_df_task()
362
+
363
+ if len(df_task) == 0:
364
+ logger.info("タスクが0件ですが、ヘッダ行を出力します。")
365
+ # aggregate_df関数と同じ列構成の空のDataFrameを作成
366
+ metadata_columns = [f"metadata.{key}" for key in (metadata_keys or [])]
367
+ result_columns = [
368
+ "phase",
369
+ *metadata_columns,
370
+ TaskStatusForSummary.NEVER_WORKED_UNASSIGNED.value,
371
+ TaskStatusForSummary.NEVER_WORKED_ASSIGNED.value,
372
+ TaskStatusForSummary.WORKED_NOT_REJECTED.value,
373
+ TaskStatusForSummary.WORKED_REJECTED.value,
374
+ TaskStatusForSummary.ON_HOLD.value,
375
+ TaskStatusForSummary.COMPLETE.value,
376
+ ]
377
+ df_summary = pandas.DataFrame(columns=result_columns)
378
+ else:
379
+ logger.info(f"{len(df_task)} 件のタスクを集計しました。")
380
+ df_summary = aggregate_df(df_task, metadata_keys)
381
+
382
+ self.print_csv(df_summary)
383
+ logger.info(f"project_id='{project_id}' :: フェーズごとのタスク数をCSV形式で出力しました。")
384
+
385
+ def main(self) -> None:
386
+ args = self.args
387
+ project_id = args.project_id
388
+ temp_dir = Path(args.temp_dir) if args.temp_dir is not None else None
389
+
390
+ self.list_task_count_by_phase(
391
+ project_id,
392
+ temp_dir=temp_dir,
393
+ should_execute_get_tasks_api=args.execute_get_tasks_api,
394
+ not_worked_threshold_second=args.not_worked_threshold_second,
395
+ metadata_keys=args.metadata_key,
396
+ )
397
+
398
+
399
+ def parse_args(parser: argparse.ArgumentParser) -> None:
400
+ argument_parser = ArgumentParser(parser)
401
+
402
+ argument_parser.add_project_id()
403
+
404
+ parser.add_argument(
405
+ "--execute_get_tasks_api",
406
+ action="store_true",
407
+ help="タスク全件ファイルをダウンロードせずに、`getTasks` APIを実行してタスク一覧を取得します。`getTasks` APIを複数回実行するので、タスク全件ファイルをダウンロードするよりも時間がかかります。",
408
+ )
409
+
410
+ parser.add_argument(
411
+ "--temp_dir",
412
+ type=str,
413
+ help="指定したディレクトリに、一時ファイルをダウンロードします。",
414
+ )
415
+
416
+ parser.add_argument(
417
+ "--not_worked_threshold_second",
418
+ type=float,
419
+ default=0,
420
+ help="作業していないとみなす作業時間の閾値を秒単位で指定します。この値以下の作業時間のタスクは、作業していないとみなします。",
421
+ )
422
+
423
+ parser.add_argument(
424
+ "--metadata_key",
425
+ type=str,
426
+ nargs="+",
427
+ help="集計対象のメタデータキーを指定します。指定したキーの値でグループ化してタスク数を集計します。",
428
+ )
429
+
430
+ argument_parser.add_output()
431
+
432
+ parser.set_defaults(subcommand_func=main)
433
+
434
+
435
+ def main(args: argparse.Namespace) -> None:
436
+ service = build_annofabapi_resource_and_login(args)
437
+ facade = AnnofabApiFacade(service)
438
+ ListTaskCountByPhase(service, facade, args).main()
439
+
440
+
441
+ def add_parser(subparsers: argparse._SubParsersAction | None = None) -> argparse.ArgumentParser:
442
+ subcommand_name = "list_by_phase"
443
+ subcommand_help = "フェーズごとのタスク数をCSV形式で出力します。"
444
+ epilog = "オーナロールまたはアノテーションユーザーロールを持つユーザで実行してください。"
445
+ parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, epilog=epilog)
446
+ parse_args(parser)
447
+ return parser
@@ -0,0 +1,21 @@
1
+ import argparse
2
+
3
+ import annofabcli.common.cli
4
+ import annofabcli.task_count.list_by_phase
5
+
6
+
7
+ def parse_args(parser: argparse.ArgumentParser) -> None:
8
+ subparsers = parser.add_subparsers(dest="subcommand_name")
9
+
10
+ # サブコマンドの定義
11
+ annofabcli.task_count.list_by_phase.add_parser(subparsers)
12
+
13
+
14
+ def add_parser(subparsers: argparse._SubParsersAction | None = None) -> argparse.ArgumentParser:
15
+ subcommand_name = "task_count"
16
+ subcommand_help = "タスク数関係のサブコマンド"
17
+ description = "タスク数関係のサブコマンド"
18
+
19
+ parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, is_subcommand=False)
20
+ parse_args(parser)
21
+ return parser
@@ -39,21 +39,34 @@ class ListTaskHistoryWithJsonMain:
39
39
  filtered_task_history_dict[task_id] = task_history_list
40
40
  return filtered_task_history_dict
41
41
 
42
- def get_task_history_dict(self, project_id: str, task_history_json: Path | None = None, task_id_list: list[str] | None = None) -> TaskHistoryDict:
42
+ def get_task_history_dict(self, project_id: str, task_history_json: Path | None = None, task_id_list: list[str] | None = None, temp_dir: Path | None = None) -> TaskHistoryDict:
43
43
  """出力対象のタスク履歴情報を取得する"""
44
44
  if task_history_json is None:
45
45
  downloading_obj = DownloadingFile(self.service)
46
46
  # `NamedTemporaryFile`を使わない理由: Windowsで`PermissionError`が発生するため
47
47
  # https://qiita.com/yuji38kwmt/items/c6f50e1fc03dafdcdda0 参考
48
- with tempfile.TemporaryDirectory() as str_temp_dir:
49
- tmp_json_path = Path(str_temp_dir) / "task_history.json"
50
- downloading_obj.download_task_history_json(project_id, str(tmp_json_path))
51
- with tmp_json_path.open(encoding="utf-8") as f:
52
- all_task_history_dict = json.load(f)
48
+ if temp_dir is not None:
49
+ tmp_json_path = downloading_obj.download_task_history_json_to_dir(project_id, temp_dir)
50
+ else:
51
+ with tempfile.TemporaryDirectory() as str_temp_dir:
52
+ tmp_json_path = downloading_obj.download_task_history_json_to_dir(project_id, Path(str_temp_dir))
53
+ with tmp_json_path.open(encoding="utf-8") as f:
54
+ all_task_history_dict = json.load(f)
55
+ # 一時ディレクトリの場合はここでフィルタリング処理まで行う
56
+ task_history_dict = self.filter_task_history_dict(all_task_history_dict, task_id_list)
57
+
58
+ visualize = AddProps(self.service, project_id)
53
59
 
60
+ for task_history_list in task_history_dict.values():
61
+ for task_history in task_history_list:
62
+ visualize.add_properties_to_task_history(task_history)
63
+
64
+ return task_history_dict
54
65
  else:
55
- with task_history_json.open(encoding="utf-8") as f:
56
- all_task_history_dict = json.load(f)
66
+ tmp_json_path = task_history_json
67
+
68
+ with tmp_json_path.open(encoding="utf-8") as f:
69
+ all_task_history_dict = json.load(f)
57
70
 
58
71
  task_history_dict = self.filter_task_history_dict(all_task_history_dict, task_id_list)
59
72
 
@@ -80,6 +93,7 @@ class ListTaskHistoryWithJson(CommandLine):
80
93
  task_history_json: Path | None,
81
94
  task_id_list: list[str] | None,
82
95
  arg_format: FormatArgument,
96
+ temp_dir: Path | None,
83
97
  ):
84
98
  """
85
99
  タスク一覧を出力する
@@ -95,7 +109,7 @@ class ListTaskHistoryWithJson(CommandLine):
95
109
  super().validate_project(project_id, project_member_roles=None)
96
110
 
97
111
  main_obj = ListTaskHistoryWithJsonMain(self.service)
98
- task_history_dict = main_obj.get_task_history_dict(project_id, task_history_json=task_history_json, task_id_list=task_id_list)
112
+ task_history_dict = main_obj.get_task_history_dict(project_id, task_history_json=task_history_json, task_id_list=task_id_list, temp_dir=temp_dir)
99
113
  logger.debug(f"{len(task_history_dict)} 件のタスクの履歴情報を出力します。")
100
114
  if arg_format == FormatArgument.CSV:
101
115
  all_task_history_list = main_obj.to_all_task_history_list_from_dict(task_history_dict)
@@ -107,12 +121,14 @@ class ListTaskHistoryWithJson(CommandLine):
107
121
  args = self.args
108
122
 
109
123
  task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id) if args.task_id is not None else None
124
+ temp_dir = Path(args.temp_dir) if args.temp_dir is not None else None
110
125
 
111
126
  self.print_task_history_list(
112
127
  args.project_id,
113
128
  task_history_json=args.task_history_json,
114
129
  task_id_list=task_id_list,
115
130
  arg_format=FormatArgument(args.format),
131
+ temp_dir=temp_dir,
116
132
  )
117
133
 
118
134
 
@@ -141,6 +157,12 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
141
157
  "JSONファイルは ``$ annofabcli task_history download`` コマンドで取得できます。",
142
158
  )
143
159
 
160
+ parser.add_argument(
161
+ "--temp_dir",
162
+ type=str,
163
+ help="``--task_history_json`` を指定しなかった場合、ダウンロードしたJSONファイルの保存先ディレクトリを指定できます。指定しない場合は、一時ディレクトリに保存されます。",
164
+ )
165
+
144
166
  argument_parser.add_format(
145
167
  choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON],
146
168
  default=FormatArgument.CSV,
@@ -43,20 +43,32 @@ class ListTaskHistoryEventWithJsonMain:
43
43
 
44
44
  return task_history_event_list
45
45
 
46
- def get_task_history_event_list(self, project_id: str, task_history_event_json: Path | None = None, task_id_list: list[str] | None = None) -> list[dict[str, Any]]:
46
+ def get_task_history_event_list(self, project_id: str, task_history_event_json: Path | None = None, task_id_list: list[str] | None = None, temp_dir: Path | None = None) -> list[dict[str, Any]]:
47
47
  if task_history_event_json is None:
48
48
  downloading_obj = DownloadingFile(self.service)
49
49
  # `NamedTemporaryFile`を使わない理由: Windowsで`PermissionError`が発生するため
50
50
  # https://qiita.com/yuji38kwmt/items/c6f50e1fc03dafdcdda0 参考
51
- with tempfile.TemporaryDirectory() as str_temp_dir:
52
- tmp_json_file = Path(str_temp_dir) / "task_history_event.json"
53
- downloading_obj.download_task_history_event_json(project_id, str(tmp_json_file))
54
- with tmp_json_file.open(encoding="utf-8") as f:
55
- all_task_history_event_list = json.load(f)
51
+ if temp_dir is not None:
52
+ tmp_json_file = downloading_obj.download_task_history_event_json_to_dir(project_id, temp_dir)
53
+ else:
54
+ with tempfile.TemporaryDirectory() as str_temp_dir:
55
+ tmp_json_file = downloading_obj.download_task_history_event_json_to_dir(project_id, Path(str_temp_dir))
56
+ with tmp_json_file.open(encoding="utf-8") as f:
57
+ all_task_history_event_list = json.load(f)
58
+ # 一時ディレクトリの場合はここでフィルタリング処理まで行う
59
+ filtered_task_history_event_list = self.filter_task_history_event(all_task_history_event_list, task_id_list)
60
+
61
+ visualize = AddProps(self.service, project_id)
56
62
 
63
+ for event in filtered_task_history_event_list:
64
+ visualize.add_properties_to_task_history_event(event)
65
+
66
+ return filtered_task_history_event_list
57
67
  else:
58
- with task_history_event_json.open(encoding="utf-8") as f:
59
- all_task_history_event_list = json.load(f)
68
+ tmp_json_file = task_history_event_json
69
+
70
+ with tmp_json_file.open(encoding="utf-8") as f:
71
+ all_task_history_event_list = json.load(f)
60
72
 
61
73
  filtered_task_history_event_list = self.filter_task_history_event(all_task_history_event_list, task_id_list)
62
74
 
@@ -75,11 +87,12 @@ class ListTaskHistoryEventWithJson(CommandLine):
75
87
  task_history_event_json: Path | None,
76
88
  task_id_list: list[str] | None,
77
89
  arg_format: FormatArgument,
90
+ temp_dir: Path | None,
78
91
  ) -> None:
79
92
  super().validate_project(project_id, project_member_roles=None)
80
93
 
81
94
  main_obj = ListTaskHistoryEventWithJsonMain(self.service)
82
- task_history_event_list = main_obj.get_task_history_event_list(project_id, task_history_event_json=task_history_event_json, task_id_list=task_id_list)
95
+ task_history_event_list = main_obj.get_task_history_event_list(project_id, task_history_event_json=task_history_event_json, task_id_list=task_id_list, temp_dir=temp_dir)
83
96
 
84
97
  logger.debug(f"{len(task_history_event_list)} 件のタスク履歴イベントの情報を出力します。")
85
98
 
@@ -114,12 +127,14 @@ class ListTaskHistoryEventWithJson(CommandLine):
114
127
  args = self.args
115
128
 
116
129
  task_id_list = get_list_from_args(args.task_id) if args.task_id is not None else None
130
+ temp_dir = Path(args.temp_dir) if args.temp_dir is not None else None
117
131
 
118
132
  self.print_task_history_event_list(
119
133
  args.project_id,
120
134
  task_history_event_json=args.task_history_event_json,
121
135
  task_id_list=task_id_list,
122
136
  arg_format=FormatArgument(args.format),
137
+ temp_dir=temp_dir,
123
138
  )
124
139
 
125
140
  @staticmethod
@@ -156,6 +171,12 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
156
171
  "JSONファイルは ``$ annofabcli task_history_event download`` コマンドで取得できます。",
157
172
  )
158
173
 
174
+ parser.add_argument(
175
+ "--temp_dir",
176
+ type=str,
177
+ help="``--task_history_event_json`` を指定しなかった場合、ダウンロードしたJSONファイルの保存先ディレクトリを指定できます。指定しない場合は、一時ディレクトリに保存されます。",
178
+ )
179
+
159
180
  argument_parser.add_format(
160
181
  choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON],
161
182
  default=FormatArgument.CSV,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: annofabcli
3
- Version: 1.113.0
3
+ Version: 1.114.0
4
4
  Summary: Utility Command Line Interface for AnnoFab
5
5
  Author: Kurusugawa Computer Inc.
6
6
  License: MIT