annofabcli 1.113.1__py3-none-any.whl → 1.114.1__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.
annofabcli/__main__.py CHANGED
@@ -26,6 +26,7 @@ import annofabcli.stat_visualization.subcommand_stat_visualization
26
26
  import annofabcli.statistics.subcommand_statistics
27
27
  import annofabcli.supplementary.subcommand_supplementary
28
28
  import annofabcli.task.subcommand_task
29
+ import annofabcli.task_count.subcommand_task_count
29
30
  import annofabcli.task_history.subcommand_task_history
30
31
  import annofabcli.task_history_event.subcommand_task_history_event
31
32
 
@@ -121,6 +122,7 @@ def create_parser() -> argparse.ArgumentParser:
121
122
  annofabcli.stat_visualization.subcommand_stat_visualization.add_parser(subparsers)
122
123
  annofabcli.supplementary.subcommand_supplementary.add_parser(subparsers)
123
124
  annofabcli.task.subcommand_task.add_parser(subparsers)
125
+ annofabcli.task_count.subcommand_task_count.add_parser(subparsers)
124
126
  annofabcli.task_history.subcommand_task_history.add_parser(subparsers)
125
127
  annofabcli.task_history_event.subcommand_task_history_event.add_parser(subparsers)
126
128
 
@@ -39,21 +39,46 @@ class ListAllCommentMain:
39
39
  task_ids: Collection[str] | None,
40
40
  comment_type: CommentType | None,
41
41
  exclude_reply: bool, # noqa: FBT001
42
+ temp_dir: Path | None,
42
43
  ) -> list[dict[str, Any]]:
43
44
  if comment_json is None:
44
45
  downloading_obj = DownloadingFile(self.service)
45
46
  # `NamedTemporaryFile`を使わない理由: Windowsで`PermissionError`が発生するため
46
47
  # https://qiita.com/yuji38kwmt/items/c6f50e1fc03dafdcdda0 参考
47
- with tempfile.TemporaryDirectory() as str_temp_dir:
48
- json_path = Path(str_temp_dir) / f"{project_id}__comment.json"
49
- downloading_obj.download_comment_json(project_id, str(json_path))
50
- with json_path.open(encoding="utf-8") as f:
51
- comment_list = json.load(f)
52
-
48
+ if temp_dir is not None:
49
+ json_path = downloading_obj.download_comment_json_to_dir(project_id, temp_dir)
50
+ else:
51
+ with tempfile.TemporaryDirectory() as str_temp_dir:
52
+ json_path = downloading_obj.download_comment_json_to_dir(project_id, Path(str_temp_dir))
53
+ with json_path.open(encoding="utf-8") as f:
54
+ comment_list = json.load(f)
55
+ # 一時ディレクトリの場合はここでフィルタリング処理まで行う
56
+ if task_ids is not None:
57
+ task_id_set = set(task_ids)
58
+ comment_list = [e for e in comment_list if e["task_id"] in task_id_set]
59
+
60
+ if comment_type is not None:
61
+ comment_list = [e for e in comment_list if e["comment_type"] == comment_type.value]
62
+
63
+ # 返信回数を算出する
64
+ reply_counter = create_reply_counter(comment_list)
65
+ for c in comment_list:
66
+ key = (c["task_id"], c["input_data_id"], c["comment_id"])
67
+ c["reply_count"] = reply_counter.get(key, 0)
68
+
69
+ if exclude_reply:
70
+ # 返信コメントを除外する
71
+ comment_list = [e for e in comment_list if e["comment_node"]["_type"] != "Reply"]
72
+
73
+ visualize = AddProps(self.service, project_id)
74
+ comment_list = [visualize.add_properties_to_comment(e) for e in comment_list]
75
+
76
+ return comment_list
53
77
  else:
54
78
  json_path = comment_json
55
- with json_path.open(encoding="utf-8") as f:
56
- comment_list = json.load(f)
79
+
80
+ with json_path.open(encoding="utf-8") as f:
81
+ comment_list = json.load(f)
57
82
 
58
83
  if task_ids is not None:
59
84
  task_id_set = set(task_ids)
@@ -86,6 +111,7 @@ class ListAllComment(CommandLine):
86
111
 
87
112
  task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id) if args.task_id is not None else None
88
113
  comment_type = CommentType(args.comment_type) if args.comment_type is not None else None
114
+ temp_dir = Path(args.temp_dir) if args.temp_dir is not None else None
89
115
 
90
116
  main_obj = ListAllCommentMain(self.service)
91
117
  comment_list = main_obj.get_all_comment(
@@ -94,6 +120,7 @@ class ListAllComment(CommandLine):
94
120
  task_ids=task_id_list,
95
121
  comment_type=comment_type,
96
122
  exclude_reply=args.exclude_reply,
123
+ temp_dir=temp_dir,
97
124
  )
98
125
 
99
126
  logger.info(f"コメントの件数: {len(comment_list)}")
@@ -133,6 +160,12 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
133
160
 
134
161
  parser.add_argument("--exclude_reply", action="store_true", help="返信コメントを除外します。")
135
162
 
163
+ parser.add_argument(
164
+ "--temp_dir",
165
+ type=str,
166
+ help="``--comment_json`` を指定しなかった場合、ダウンロードしたJSONファイルの保存先ディレクトリを指定できます。指定しない場合は、一時ディレクトリに保存されます。",
167
+ )
168
+
136
169
  argument_parser.add_format(
137
170
  choices=[
138
171
  FormatArgument.CSV,
@@ -404,3 +404,72 @@ class DownloadingFile:
404
404
  wait_options=wait_options,
405
405
  )
406
406
  return dest_path
407
+
408
+ def download_task_history_json_to_dir(
409
+ self,
410
+ project_id: str,
411
+ output_dir: Path,
412
+ ) -> Path:
413
+ """
414
+ タスク履歴JSONをoutput_dirに統一された命名規則でダウンロードする。
415
+
416
+ Args:
417
+ project_id: プロジェクトID
418
+ output_dir: 出力ディレクトリ
419
+
420
+ Returns:
421
+ ダウンロードされたファイルのパス
422
+ """
423
+ dest_path = output_dir / get_filename(project_id, "task_history", "json")
424
+
425
+ self.download_task_history_json(
426
+ project_id,
427
+ dest_path=str(dest_path),
428
+ )
429
+ return dest_path
430
+
431
+ def download_task_history_event_json_to_dir(
432
+ self,
433
+ project_id: str,
434
+ output_dir: Path,
435
+ ) -> Path:
436
+ """
437
+ タスク履歴イベントJSONをoutput_dirに統一された命名規則でダウンロードする。
438
+
439
+ Args:
440
+ project_id: プロジェクトID
441
+ output_dir: 出力ディレクトリ
442
+
443
+ Returns:
444
+ ダウンロードされたファイルのパス
445
+ """
446
+ dest_path = output_dir / get_filename(project_id, "task_history_event", "json")
447
+
448
+ self.download_task_history_event_json(
449
+ project_id,
450
+ dest_path=str(dest_path),
451
+ )
452
+ return dest_path
453
+
454
+ def download_comment_json_to_dir(
455
+ self,
456
+ project_id: str,
457
+ output_dir: Path,
458
+ ) -> Path:
459
+ """
460
+ コメントJSONをoutput_dirに統一された命名規則でダウンロードする。
461
+
462
+ Args:
463
+ project_id: プロジェクトID
464
+ output_dir: 出力ディレクトリ
465
+
466
+ Returns:
467
+ ダウンロードされたファイルのパス
468
+ """
469
+ dest_path = output_dir / get_filename(project_id, "comment", "json")
470
+
471
+ self.download_comment_json(
472
+ project_id,
473
+ dest_path=str(dest_path),
474
+ )
475
+ return dest_path
@@ -71,24 +71,39 @@ class ListInputDataWithJsonMain:
71
71
  contain_parent_task_id_list: bool = False,
72
72
  contain_supplementary_data_count: bool = False,
73
73
  is_latest: bool = False,
74
+ temp_dir: Path | None = None,
74
75
  ) -> list[dict[str, Any]]:
75
76
  if input_data_json is None:
76
77
  downloading_obj = DownloadingFile(self.service)
77
78
  # `NamedTemporaryFile`を使わない理由: Windowsで`PermissionError`が発生するため
78
79
  # https://qiita.com/yuji38kwmt/items/c6f50e1fc03dafdcdda0 参考
79
- with tempfile.TemporaryDirectory() as str_temp_dir:
80
- json_path = Path(str_temp_dir) / f"{project_id}__input_data.json"
81
- downloading_obj.download_input_data_json(
82
- project_id,
83
- str(json_path),
84
- is_latest=is_latest,
85
- )
86
- with json_path.open(encoding="utf-8") as f:
87
- input_data_list = json.load(f)
80
+ if temp_dir is not None:
81
+ json_path = downloading_obj.download_input_data_json_to_dir(project_id, temp_dir, is_latest=is_latest)
82
+ else:
83
+ with tempfile.TemporaryDirectory() as str_temp_dir:
84
+ json_path = downloading_obj.download_input_data_json_to_dir(project_id, Path(str_temp_dir), is_latest=is_latest)
85
+ with json_path.open(encoding="utf-8") as f:
86
+ input_data_list = json.load(f)
87
+ # 一時ディレクトリの場合はここでフィルタリング処理まで行う
88
+ input_data_id_set = set(input_data_id_list) if input_data_id_list is not None else None
89
+ filtered_input_data_list = [e for e in input_data_list if self.filter_input_data_list(e, input_data_query=input_data_query, input_data_id_set=input_data_id_set)]
90
+
91
+ adding_obj = AddingDetailsToInputData(self.service, project_id)
92
+ if contain_parent_task_id_list:
93
+ adding_obj.add_parent_task_id_list_to_input_data_list(input_data_list)
94
+
95
+ if contain_supplementary_data_count:
96
+ adding_obj.add_supplementary_data_count_to_input_data_list(input_data_list)
97
+
98
+ # 入力データの不要なキーを削除する
99
+ for input_data in input_data_list:
100
+ remove_unnecessary_keys_from_input_data(input_data)
101
+ return filtered_input_data_list
88
102
  else:
89
103
  json_path = input_data_json
90
- with json_path.open(encoding="utf-8") as f:
91
- input_data_list = json.load(f)
104
+
105
+ with json_path.open(encoding="utf-8") as f:
106
+ input_data_list = json.load(f)
92
107
 
93
108
  input_data_id_set = set(input_data_id_list) if input_data_id_list is not None else None
94
109
  filtered_input_data_list = [e for e in input_data_list if self.filter_input_data_list(e, input_data_query=input_data_query, input_data_id_set=input_data_id_set)]
@@ -117,6 +132,7 @@ class ListAllInputData(CommandLine):
117
132
  super().validate_project(project_id, project_member_roles=[ProjectMemberRole.TRAINING_DATA_USER, ProjectMemberRole.OWNER])
118
133
 
119
134
  main_obj = ListInputDataWithJsonMain(self.service)
135
+ temp_dir = Path(args.temp_dir) if args.temp_dir is not None else None
120
136
  input_data_list = main_obj.get_input_data_list(
121
137
  project_id=project_id,
122
138
  input_data_json=args.input_data_json,
@@ -125,6 +141,7 @@ class ListAllInputData(CommandLine):
125
141
  is_latest=args.latest,
126
142
  contain_parent_task_id_list=args.with_parent_task_id_list,
127
143
  contain_supplementary_data_count=args.with_supplementary_data_count,
144
+ temp_dir=temp_dir,
128
145
  )
129
146
 
130
147
  logger.debug(f"入力データ一覧の件数: {len(input_data_list)}")
@@ -195,6 +212,12 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
195
212
 
196
213
  parser.add_argument("--with_supplementary_data_count", action="store_true", help="入力データに紐づく補助情報の個数( ``supplementary_data_count`` )も出力します。")
197
214
 
215
+ parser.add_argument(
216
+ "--temp_dir",
217
+ type=str,
218
+ help="``--input_data_json`` を指定しなかった場合、ダウンロードしたJSONファイルの保存先ディレクトリを指定できます。指定しない場合は、一時ディレクトリに保存されます。",
219
+ )
220
+
198
221
  argument_parser.add_format(
199
222
  choices=[
200
223
  FormatArgument.CSV,
@@ -47,21 +47,34 @@ class ListTasksWithJsonMain:
47
47
  task_id_list: list[str] | None = None,
48
48
  task_query: TaskQuery | None = None,
49
49
  is_latest: bool = False, # noqa: FBT001, FBT002
50
+ temp_dir: Path | None = None,
50
51
  ) -> list[dict[str, Any]]:
51
52
  if task_json is None:
52
53
  downloading_obj = DownloadingFile(self.service)
53
54
  # `NamedTemporaryFile`を使わない理由: Windowsで`PermissionError`が発生するため
54
55
  # https://qiita.com/yuji38kwmt/items/c6f50e1fc03dafdcdda0 参考
55
- with tempfile.TemporaryDirectory() as str_temp_dir:
56
- json_path = Path(str_temp_dir) / f"{project_id}__task.json"
57
- downloading_obj.download_task_json(project_id, str(json_path), is_latest=is_latest)
58
- with json_path.open(encoding="utf-8") as f:
59
- task_list = json.load(f)
60
-
56
+ if temp_dir is not None:
57
+ json_path = downloading_obj.download_task_json_to_dir(project_id, temp_dir, is_latest=is_latest)
58
+ else:
59
+ with tempfile.TemporaryDirectory() as str_temp_dir:
60
+ json_path = downloading_obj.download_task_json_to_dir(project_id, Path(str_temp_dir), is_latest=is_latest)
61
+ with json_path.open(encoding="utf-8") as f:
62
+ task_list = json.load(f)
63
+ # 一時ディレクトリの場合はここでフィルタリング処理まで行う
64
+ if task_query is not None:
65
+ task_query = self.facade.set_account_id_of_task_query(project_id, task_query)
66
+
67
+ logger.debug("出力対象のタスクを抽出しています。")
68
+ task_id_set = set(task_id_list) if task_id_list is not None else None
69
+ filtered_task_list = [e for e in task_list if self.match_task_with_conditions(e, task_query=task_query, task_id_set=task_id_set)]
70
+
71
+ visualize_obj = AddProps(self.service, project_id)
72
+ return [visualize_obj.add_properties_to_task(e) for e in filtered_task_list]
61
73
  else:
62
74
  json_path = task_json
63
- with json_path.open(encoding="utf-8") as f:
64
- task_list = json.load(f)
75
+
76
+ with json_path.open(encoding="utf-8") as f:
77
+ task_list = json.load(f)
65
78
 
66
79
  if task_query is not None:
67
80
  task_query = self.facade.set_account_id_of_task_query(project_id, task_query)
@@ -85,12 +98,14 @@ class ListTasksWithJson(CommandLine):
85
98
  super().validate_project(project_id, project_member_roles=None)
86
99
 
87
100
  main_obj = ListTasksWithJsonMain(self.service)
101
+ temp_dir = Path(args.temp_dir) if args.temp_dir is not None else None
88
102
  task_list = main_obj.get_task_list(
89
103
  project_id=project_id,
90
104
  task_json=args.task_json,
91
105
  task_id_list=task_id_list,
92
106
  task_query=task_query,
93
107
  is_latest=args.latest,
108
+ temp_dir=temp_dir,
94
109
  )
95
110
 
96
111
  logger.debug(f"タスク一覧の件数: {len(task_list)}")
@@ -133,6 +148,12 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
133
148
  "指定しない場合は、コマンドを実行した日の02:00(JST)頃のタスクの一覧が出力されます。",
134
149
  )
135
150
 
151
+ parser.add_argument(
152
+ "--temp_dir",
153
+ type=str,
154
+ help="``--task_json`` を指定しなかった場合、ダウンロードしたJSONファイルの保存先ディレクトリを指定できます。指定しない場合は、一時ディレクトリに保存されます。",
155
+ )
156
+
136
157
  argument_parser.add_format(
137
158
  choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON, FormatArgument.TASK_ID_LIST],
138
159
  default=FormatArgument.CSV,
@@ -60,31 +60,35 @@ class ListAllTasksAddedTaskHistoryMain:
60
60
 
61
61
  return task_list
62
62
 
63
- def load_task_list(self, task_json_path: Path | None) -> list[dict[str, Any]]:
64
- if task_json_path is not None:
65
- with task_json_path.open(encoding="utf-8") as f:
66
- return json.load(f)
67
-
68
- # `NamedTemporaryFile`を使わない理由: Windowsで`PermissionError`が発生するため
69
- # https://qiita.com/yuji38kwmt/items/c6f50e1fc03dafdcdda0 参考
70
- with tempfile.TemporaryDirectory() as str_temp_dir:
71
- task_json_path = Path(str_temp_dir) / f"{self.project_id}__task.json"
72
- self.downloading_obj.download_task_json(self.project_id, str(task_json_path))
73
- with task_json_path.open(encoding="utf-8") as f:
74
- return json.load(f)
75
-
76
- def load_task_history_dict(self, task_history_json_path: Path | None) -> TaskHistoryDict:
77
- if task_history_json_path is not None:
78
- with task_history_json_path.open(encoding="utf-8") as f:
79
- return json.load(f)
80
- else:
63
+ def load_task_list(self, task_json_path: Path | None, temp_dir: Path | None) -> list[dict[str, Any]]:
64
+ if task_json_path is None:
81
65
  # `NamedTemporaryFile`を使わない理由: Windowsで`PermissionError`が発生するため
82
66
  # https://qiita.com/yuji38kwmt/items/c6f50e1fc03dafdcdda0 参考
83
- with tempfile.TemporaryDirectory() as str_temp_dir:
84
- task_history_json_path = Path(str_temp_dir) / f"{self.project_id}__task_history.json"
85
- self.downloading_obj.download_task_history_json(self.project_id, str(task_history_json_path))
86
- with task_history_json_path.open(encoding="utf-8") as f:
87
- return json.load(f)
67
+ if temp_dir is not None:
68
+ task_json_path = self.downloading_obj.download_task_json_to_dir(self.project_id, temp_dir)
69
+ else:
70
+ with tempfile.TemporaryDirectory() as str_temp_dir:
71
+ task_json_path = self.downloading_obj.download_task_json_to_dir(self.project_id, Path(str_temp_dir))
72
+ with task_json_path.open(encoding="utf-8") as f:
73
+ return json.load(f)
74
+
75
+ with task_json_path.open(encoding="utf-8") as f:
76
+ return json.load(f)
77
+
78
+ def load_task_history_dict(self, task_history_json_path: Path | None, temp_dir: Path | None) -> TaskHistoryDict:
79
+ if task_history_json_path is None:
80
+ # `NamedTemporaryFile`を使わない理由: Windowsで`PermissionError`が発生するため
81
+ # https://qiita.com/yuji38kwmt/items/c6f50e1fc03dafdcdda0 参考
82
+ if temp_dir is not None:
83
+ task_history_json_path = self.downloading_obj.download_task_history_json_to_dir(self.project_id, temp_dir)
84
+ else:
85
+ with tempfile.TemporaryDirectory() as str_temp_dir:
86
+ task_history_json_path = self.downloading_obj.download_task_history_json_to_dir(self.project_id, Path(str_temp_dir))
87
+ with task_history_json_path.open(encoding="utf-8") as f:
88
+ return json.load(f)
89
+
90
+ with task_history_json_path.open(encoding="utf-8") as f:
91
+ return json.load(f)
88
92
 
89
93
  @staticmethod
90
94
  def match_task_with_conditions(
@@ -120,12 +124,13 @@ class ListAllTasksAddedTaskHistoryMain:
120
124
  task_history_json_path: Path | None,
121
125
  task_id_list: list[str] | None,
122
126
  task_query: TaskQuery | None,
127
+ temp_dir: Path | None,
123
128
  ):
124
129
  """
125
130
  タスク履歴情報を加えたタスク一覧を取得する。
126
131
  """
127
- task_list = self.load_task_list(task_json_path)
128
- task_history_dict = self.load_task_history_dict(task_history_json_path)
132
+ task_list = self.load_task_list(task_json_path, temp_dir)
133
+ task_history_dict = self.load_task_history_dict(task_history_json_path, temp_dir)
129
134
 
130
135
  filtered_task_list = self.filter_task_list(task_list, task_id_list=task_id_list, task_query=task_query)
131
136
 
@@ -163,11 +168,13 @@ class ListAllTasksAddedTaskHistory(CommandLine):
163
168
 
164
169
  self.validate_project(project_id, [ProjectMemberRole.OWNER, ProjectMemberRole.TRAINING_DATA_USER])
165
170
 
171
+ temp_dir = Path(args.temp_dir) if args.temp_dir is not None else None
166
172
  task_list = ListAllTasksAddedTaskHistoryMain(self.service, project_id).get_task_list_added_task_history(
167
173
  task_json_path=args.task_json,
168
174
  task_history_json_path=args.task_history_json,
169
175
  task_id_list=task_id_list,
170
176
  task_query=task_query,
177
+ temp_dir=temp_dir,
171
178
  )
172
179
 
173
180
  logger.info(f"タスク一覧の件数: {len(task_list)}")
@@ -200,6 +207,12 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
200
207
  "JSONファイルは ``$ annofabcli task_history download`` コマンドで取得できます。",
201
208
  )
202
209
 
210
+ parser.add_argument(
211
+ "--temp_dir",
212
+ type=str,
213
+ help="``--task_json`` と ``--task_history_json`` を指定しなかった場合、ダウンロードしたJSONファイルの保存先ディレクトリを指定できます。指定しない場合は、一時ディレクトリに保存されます。",
214
+ )
215
+
203
216
  argument_parser.add_output()
204
217
 
205
218
  argument_parser.add_format(
File without changes
@@ -0,0 +1,558 @@
1
+ import argparse
2
+ import json
3
+ import logging
4
+ import sys
5
+ import tempfile
6
+ from enum import Enum
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import isodate
11
+ import pandas
12
+ from annofabapi.models import ProjectMemberRole, Task, TaskPhase, TaskStatus
13
+ from annofabapi.resource import Resource as AnnofabResource
14
+ from annofabapi.utils import get_number_of_rejections
15
+
16
+ import annofabcli.common
17
+ from annofabcli.common.cli import (
18
+ COMMAND_LINE_ERROR_STATUS_CODE,
19
+ ArgumentParser,
20
+ CommandLine,
21
+ build_annofabapi_resource_and_login,
22
+ )
23
+ from annofabcli.common.download import DownloadingFile
24
+ from annofabcli.common.facade import AnnofabApiFacade
25
+ from annofabcli.common.type_util import assert_noreturn
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class AggregationUnit(Enum):
31
+ """
32
+ 集計の単位。
33
+ """
34
+
35
+ TASK = "task_count"
36
+ """タスク数"""
37
+ INPUT_DATA = "input_data_count"
38
+ """入力データ数"""
39
+ VIDEO_DURATION_HOUR = "video_duration_hour"
40
+ """動画の長さ(時間)"""
41
+ VIDEO_DURATION_MINUTE = "video_duration_minute"
42
+ """動画の長さ(分)"""
43
+
44
+
45
+ def isoduration_to_second(duration: str) -> float:
46
+ """
47
+ ISO 8601 duration を 秒に変換する
48
+ """
49
+ return isodate.parse_duration(duration).total_seconds()
50
+
51
+
52
+ class TaskStatusForSummary(Enum):
53
+ """
54
+ Annofabユーザーがタスクについて知りたい粒度でまとめて分解した、タスクの状態。
55
+ """
56
+
57
+ NEVER_WORKED_UNASSIGNED = "never_worked.unassigned"
58
+ """一度も作業していない状態かつ担当者未割り当て"""
59
+ NEVER_WORKED_ASSIGNED = "never_worked.assigned"
60
+ """一度も作業していない状態かつ担当者割り当て済み"""
61
+ WORKED_NOT_REJECTED = "worked.not_rejected"
62
+ """タスクのstatusは作業中または休憩中 AND 次のフェーズでまだ差し戻されていない(次のフェーズに進んでいない)"""
63
+ WORKED_REJECTED = "worked.rejected"
64
+ """タスクのstatusは作業中または休憩中 AND 次のフェーズで差し戻された"""
65
+ ON_HOLD = "on_hold"
66
+ """保留中"""
67
+ COMPLETE = "complete"
68
+ """完了"""
69
+
70
+ @staticmethod
71
+ def _get_not_started_status(task: Task, task_history_list: list[dict[str, Any]], not_worked_threshold_second: float) -> "TaskStatusForSummary":
72
+ """
73
+ NOT_STARTED状態のタスクについて、一度も作業されていないか、差し戻されて未着手かを判定する。
74
+
75
+ Args:
76
+ task: タスク情報
77
+ task_history_list: タスク履歴のリスト
78
+ not_worked_threshold_second: 作業していないとみなす作業時間の閾値(秒)。この値以下なら作業していないとみなす。
79
+ """
80
+ # `number_of_inspections=1`を指定する理由:多段検査を無視して、検査フェーズが1回目かどうかを知りたいため
81
+ step = get_step_for_current_phase(task, number_of_inspections=1)
82
+ if step == 1:
83
+ phase = task["phase"]
84
+ worktime_second = sum(isoduration_to_second(history["accumulated_labor_time_milliseconds"]) for history in task_history_list if history["phase"] == phase)
85
+ if worktime_second <= not_worked_threshold_second:
86
+ # 一度も作業されていない(または閾値以下の作業時間)
87
+ account_id = task["account_id"]
88
+ if account_id is None:
89
+ return TaskStatusForSummary.NEVER_WORKED_UNASSIGNED
90
+ else:
91
+ return TaskStatusForSummary.NEVER_WORKED_ASSIGNED
92
+ else:
93
+ return TaskStatusForSummary.WORKED_NOT_REJECTED
94
+ else:
95
+ return TaskStatusForSummary.WORKED_NOT_REJECTED
96
+
97
+ @staticmethod
98
+ def _get_working_or_break_status(task: Task) -> "TaskStatusForSummary":
99
+ """
100
+ WORKINGまたはBREAK状態のタスクについて、rejectされているかどうかを判定する。
101
+ """
102
+ # `number_of_inspections=1`を指定する理由:多段検査を無視して、検査フェーズが1回目かどうかを知りたいため
103
+ step = get_step_for_current_phase(task, number_of_inspections=1)
104
+ if step == 1:
105
+ return TaskStatusForSummary.WORKED_NOT_REJECTED
106
+ else:
107
+ return TaskStatusForSummary.WORKED_REJECTED
108
+
109
+ @staticmethod
110
+ def from_task(task: dict[str, Any], task_history_list: list[dict[str, Any]], not_worked_threshold_second: float = 0) -> "TaskStatusForSummary":
111
+ """
112
+ タスク情報(dict)からインスタンスを生成します。
113
+
114
+ Args:
115
+ task: APIから取得したタスク情報. 以下のkeyが必要です。
116
+ * status
117
+ * phase
118
+ * phase_stage
119
+ * histories_by_phase
120
+ * account_id
121
+ task_history_list: タスク履歴のリスト
122
+ not_worked_threshold_second: 作業していないとみなす作業時間の閾値(秒)。この値以下なら作業していないとみなす。
123
+
124
+ """
125
+ status = TaskStatus(task["status"])
126
+ match status:
127
+ case TaskStatus.COMPLETE:
128
+ return TaskStatusForSummary.COMPLETE
129
+
130
+ case TaskStatus.ON_HOLD:
131
+ return TaskStatusForSummary.ON_HOLD
132
+
133
+ case TaskStatus.NOT_STARTED:
134
+ return TaskStatusForSummary._get_not_started_status(task, task_history_list, not_worked_threshold_second)
135
+
136
+ case TaskStatus.BREAK | TaskStatus.WORKING:
137
+ return TaskStatusForSummary._get_working_or_break_status(task)
138
+
139
+ case _:
140
+ raise RuntimeError(f"'{status}'は対象外です。")
141
+
142
+
143
+ def get_step_for_current_phase(task: Task, number_of_inspections: int) -> int:
144
+ """
145
+ 今のフェーズが何回目かを取得する。
146
+
147
+ Args:
148
+ task: 対象タスクの情報。phase, phase_stage, histories_by_phase を含む。
149
+ number_of_inspections: 対象プロジェクトの検査フェーズの回数
150
+
151
+ Returns:
152
+ 現在のフェーズのステップ数
153
+ """
154
+ current_phase = TaskPhase(task["phase"])
155
+ current_phase_stage = task["phase_stage"]
156
+ histories_by_phase = task["histories_by_phase"]
157
+
158
+ number_of_rejections_by_acceptance = get_number_of_rejections(histories_by_phase, phase=TaskPhase.ACCEPTANCE)
159
+ match current_phase:
160
+ case TaskPhase.ACCEPTANCE:
161
+ return number_of_rejections_by_acceptance + 1
162
+
163
+ case TaskPhase.ANNOTATION:
164
+ number_of_rejections_by_inspection = sum(
165
+ get_number_of_rejections(histories_by_phase, phase=TaskPhase.INSPECTION, phase_stage=phase_stage) for phase_stage in range(1, number_of_inspections + 1)
166
+ )
167
+ return number_of_rejections_by_inspection + number_of_rejections_by_acceptance + 1
168
+
169
+ case TaskPhase.INSPECTION:
170
+ number_of_rejections_by_inspection = sum(
171
+ 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)
172
+ )
173
+ return number_of_rejections_by_inspection + number_of_rejections_by_acceptance + 1
174
+
175
+ case _ as unreachable:
176
+ assert_noreturn(unreachable)
177
+
178
+
179
+ def create_df_task(
180
+ task_list: list[dict[str, Any]],
181
+ task_history_dict: dict[str, list[dict[str, Any]]],
182
+ *,
183
+ not_worked_threshold_second: float = 0,
184
+ metadata_keys: list[str] | None = None,
185
+ input_data_dict: dict[str, dict[str, Any]] | None = None,
186
+ ) -> pandas.DataFrame:
187
+ """
188
+ 以下の列が含まれたタスクのDataFrameを生成します。
189
+ * task_id
190
+ * phase
191
+ * task_status_for_summary
192
+ * input_data_count
193
+ * video_duration_hour
194
+ * video_duration_minute
195
+ * metadata.{key} (metadata_keysで指定された各キー)
196
+
197
+ Args:
198
+ task_list: タスク情報のlist
199
+ task_history_dict: タスクIDをキーとしたタスク履歴のdict
200
+ not_worked_threshold_second: 作業していないとみなす作業時間の閾値(秒)
201
+ metadata_keys: 集計対象のメタデータキーのリスト
202
+ input_data_dict: 入力データIDをキーとした入力データ情報のdict。動画時間を計算する場合に必要。
203
+
204
+ Returns:
205
+ タスク情報のDataFrame
206
+ """
207
+ metadata_keys = metadata_keys or []
208
+ input_data_dict = input_data_dict or {}
209
+
210
+ for task in task_list:
211
+ task_history = task_history_dict[task["task_id"]]
212
+ task["task_status_for_summary"] = TaskStatusForSummary.from_task(task, task_history, not_worked_threshold_second).value
213
+
214
+ # 入力データ数を計算
215
+ task["input_data_count"] = len(task["input_data_id_list"])
216
+
217
+ # 動画の長さを計算(時間)
218
+ video_duration_hour = 0
219
+ video_duration_minute = 0
220
+ for input_data_id in task["input_data_id_list"]:
221
+ if input_data_id in input_data_dict:
222
+ duration = input_data_dict[input_data_id]["system_metadata"]["input_duration"]
223
+ video_duration_hour += duration / 3600
224
+ video_duration_minute += duration / 60
225
+
226
+ task["video_duration_hour"] = video_duration_hour
227
+ task["video_duration_minute"] = video_duration_minute
228
+
229
+ # メタデータの値を抽出
230
+ metadata = task["metadata"]
231
+ for key in metadata_keys:
232
+ task[f"metadata.{key}"] = metadata[key]
233
+
234
+ columns = ["task_id", "phase", "input_data_count", "video_duration_hour", "video_duration_minute"] + [f"metadata.{key}" for key in metadata_keys] + ["task_status_for_summary"]
235
+ df = pandas.DataFrame(task_list, columns=columns)
236
+ return df
237
+
238
+
239
+ def aggregate_df(df: pandas.DataFrame, metadata_keys: list[str] | None = None, unit: AggregationUnit = AggregationUnit.TASK) -> pandas.DataFrame:
240
+ """
241
+ タスクのフェーズとステータスごとに、指定された単位で集計する。
242
+
243
+ Args:
244
+ df: 以下の列を持つDataFrame
245
+ * phase
246
+ * task_status_for_summary
247
+ * input_data_count
248
+ * video_duration_hour
249
+ * video_duration_minute
250
+ * metadata.{key} (metadata_keysで指定された各キー)
251
+ metadata_keys: 集計対象のメタデータキーのリスト
252
+ unit: 集計の単位
253
+
254
+ Returns:
255
+ indexがphase(とmetadata.*列),列がtask_status_for_summaryであるDataFrame
256
+ """
257
+ metadata_keys = metadata_keys or []
258
+ metadata_columns = [f"metadata.{key}" for key in metadata_keys]
259
+
260
+ # 集計対象の列を選択
261
+ match unit:
262
+ case AggregationUnit.TASK:
263
+ df["_aggregate_value"] = 1
264
+ case AggregationUnit.INPUT_DATA:
265
+ df["_aggregate_value"] = df["input_data_count"]
266
+ case AggregationUnit.VIDEO_DURATION_HOUR:
267
+ df["_aggregate_value"] = df["video_duration_hour"]
268
+ case AggregationUnit.VIDEO_DURATION_MINUTE:
269
+ df["_aggregate_value"] = df["video_duration_minute"]
270
+ case _ as unreachable:
271
+ assert_noreturn(unreachable)
272
+
273
+ index_columns = ["phase", *metadata_columns]
274
+ df2 = df.pivot_table(values="_aggregate_value", index=index_columns, columns="task_status_for_summary", aggfunc="sum", fill_value=0)
275
+
276
+ # 列数を固定する
277
+ for status in TaskStatusForSummary:
278
+ if status.value not in df2.columns:
279
+ df2[status.value] = 0
280
+
281
+ # ソート処理
282
+ sorted_phase = [
283
+ TaskPhase.ANNOTATION.value,
284
+ TaskPhase.INSPECTION.value,
285
+ TaskPhase.ACCEPTANCE.value,
286
+ ]
287
+
288
+ if len(metadata_columns) > 0:
289
+ # メタデータ列がある場合は、phase列でソートし、その後メタデータ列でソート
290
+ df2 = df2.reset_index()
291
+ df2["_phase_order"] = df2["phase"].map(lambda x: sorted_phase.index(x) if x in sorted_phase else len(sorted_phase))
292
+ df2 = df2.sort_values(["_phase_order", *metadata_columns])
293
+ df2 = df2.drop(columns=["_phase_order"])
294
+ else:
295
+ # メタデータ列がない場合は既存のロジック
296
+ new_index = sorted(df2.index, key=lambda x: sorted_phase.index(x) if x in sorted_phase else len(sorted_phase))
297
+ df2 = df2.loc[new_index]
298
+ df2 = df2.reset_index()
299
+
300
+ # 列の順序を設定
301
+ result_columns = [
302
+ "phase",
303
+ *metadata_columns,
304
+ TaskStatusForSummary.NEVER_WORKED_UNASSIGNED.value,
305
+ TaskStatusForSummary.NEVER_WORKED_ASSIGNED.value,
306
+ TaskStatusForSummary.WORKED_NOT_REJECTED.value,
307
+ TaskStatusForSummary.WORKED_REJECTED.value,
308
+ TaskStatusForSummary.ON_HOLD.value,
309
+ TaskStatusForSummary.COMPLETE.value,
310
+ ]
311
+ df2 = df2[result_columns]
312
+ return df2
313
+
314
+
315
+ class GettingTaskCountSummary:
316
+ """
317
+ タスク数のサマリーを取得するクラス
318
+ """
319
+
320
+ def __init__(
321
+ self,
322
+ annofab_service: AnnofabResource,
323
+ project_id: str,
324
+ temp_dir: Path,
325
+ *,
326
+ should_execute_get_tasks_api: bool = False,
327
+ not_worked_threshold_second: float = 0,
328
+ metadata_keys: list[str] | None = None,
329
+ unit: AggregationUnit = AggregationUnit.TASK,
330
+ ) -> None:
331
+ self.annofab_service = annofab_service
332
+ self.project_id = project_id
333
+ self.temp_dir = temp_dir
334
+ self.should_execute_get_tasks_api = should_execute_get_tasks_api
335
+ self.not_worked_threshold_second = not_worked_threshold_second
336
+ self.metadata_keys = metadata_keys or []
337
+ self.unit = unit
338
+
339
+ def create_df_task(self) -> pandas.DataFrame:
340
+ """
341
+ 以下の列が含まれたタスクのDataFrameを生成します。
342
+ * task_id
343
+ * phase
344
+ * task_status_for_summary
345
+ * input_data_count
346
+ * video_duration_hour
347
+ * video_duration_minute
348
+ * metadata.{key} (metadata_keys で指定した各メタデータキーに対応する列)
349
+ """
350
+ if self.should_execute_get_tasks_api:
351
+ task_list = self.annofab_service.wrapper.get_all_tasks(self.project_id)
352
+ task_history_dict = {}
353
+ for index, task in enumerate(task_list, start=1):
354
+ task_id = task["task_id"]
355
+ task_history_dict[task_id], _ = self.annofab_service.api.get_task_histories(self.project_id, task_id)
356
+ if index % 100 == 0:
357
+ logger.info(f"{index} 件目のタスク履歴を取得しました。")
358
+
359
+ else:
360
+ task_list = self.get_task_list_with_downloading()
361
+ task_history_dict = self.get_task_history_with_downloading()
362
+
363
+ # 動画時間を集計する場合は入力データJSONをダウンロード
364
+ input_data_dict = None
365
+ if self.unit in (AggregationUnit.VIDEO_DURATION_HOUR, AggregationUnit.VIDEO_DURATION_MINUTE):
366
+ input_data_dict = self.get_input_data_dict_with_downloading()
367
+
368
+ df = create_df_task(task_list, task_history_dict, not_worked_threshold_second=self.not_worked_threshold_second, metadata_keys=self.metadata_keys, input_data_dict=input_data_dict)
369
+ return df
370
+
371
+ def get_task_list_with_downloading(self) -> list[dict[str, Any]]:
372
+ """
373
+ タスク全件ファイルをダウンロードしてタスク情報を取得する。
374
+ """
375
+ downloading_obj = DownloadingFile(self.annofab_service)
376
+ task_json = downloading_obj.download_task_json_to_dir(self.project_id, self.temp_dir)
377
+ with task_json.open(encoding="utf-8") as f:
378
+ return json.load(f)
379
+
380
+ def get_task_history_with_downloading(self) -> dict[str, list[dict[str, Any]]]:
381
+ """
382
+ タスク履歴全件ファイルをダウンロードしてタスク情報を取得する。
383
+ """
384
+ downloading_obj = DownloadingFile(self.annofab_service)
385
+ task_history_json = downloading_obj.download_task_history_json_to_dir(self.project_id, self.temp_dir)
386
+ with task_history_json.open(encoding="utf-8") as f:
387
+ return json.load(f)
388
+
389
+ def get_input_data_dict_with_downloading(self) -> dict[str, dict[str, Any]]:
390
+ """
391
+ 入力データJSONをダウンロードして、入力データIDをキーとした辞書を返す。
392
+ """
393
+ downloading_obj = DownloadingFile(self.annofab_service)
394
+ input_data_json = downloading_obj.download_input_data_json_to_dir(self.project_id, self.temp_dir)
395
+ with input_data_json.open(encoding="utf-8") as f:
396
+ input_data_list = json.load(f)
397
+ return {input_data["input_data_id"]: input_data for input_data in input_data_list}
398
+
399
+
400
+ class ListTaskCountByPhase(CommandLine):
401
+ """
402
+ フェーズごとにタスク数や入力データ数などを集計し、CSV形式で出力する。
403
+ """
404
+
405
+ def list_task_count_by_phase(
406
+ self,
407
+ project_id: str,
408
+ *,
409
+ temp_dir: Path | None = None,
410
+ should_execute_get_tasks_api: bool = False,
411
+ not_worked_threshold_second: float = 0,
412
+ metadata_keys: list[str] | None = None,
413
+ unit: AggregationUnit = AggregationUnit.TASK,
414
+ ) -> None:
415
+ """
416
+ フェーズごとにタスク数や入力データ数などを集計し、CSV形式で出力する。
417
+
418
+ Args:
419
+ project_id: プロジェクトID
420
+ temp_dir: 一時ファイルの保存先ディレクトリ
421
+ should_execute_get_tasks_api: getTasks APIを実行するかどうか
422
+ not_worked_threshold_second: 作業していないとみなす作業時間の閾値(秒)
423
+ metadata_keys: 集計対象のメタデータキーのリスト
424
+ unit: 集計の単位
425
+ """
426
+
427
+ logger.info(f"project_id='{project_id}' :: フェーズごとの'{unit.value}'を集計します。")
428
+
429
+ if temp_dir is not None:
430
+ getting_obj = GettingTaskCountSummary(
431
+ self.service,
432
+ project_id,
433
+ temp_dir,
434
+ should_execute_get_tasks_api=should_execute_get_tasks_api,
435
+ not_worked_threshold_second=not_worked_threshold_second,
436
+ metadata_keys=metadata_keys,
437
+ unit=unit,
438
+ )
439
+ df_task = getting_obj.create_df_task()
440
+ else:
441
+ with tempfile.TemporaryDirectory() as str_temp_dir:
442
+ getting_obj = GettingTaskCountSummary(
443
+ self.service,
444
+ project_id,
445
+ Path(str_temp_dir),
446
+ should_execute_get_tasks_api=should_execute_get_tasks_api,
447
+ not_worked_threshold_second=not_worked_threshold_second,
448
+ metadata_keys=metadata_keys,
449
+ unit=unit,
450
+ )
451
+ df_task = getting_obj.create_df_task()
452
+
453
+ if len(df_task) == 0:
454
+ logger.info("タスクが0件ですが、ヘッダ行を出力します。")
455
+ # aggregate_df関数と同じ列構成の空のDataFrameを作成
456
+ metadata_columns = [f"metadata.{key}" for key in (metadata_keys or [])]
457
+ result_columns = [
458
+ "phase",
459
+ *metadata_columns,
460
+ TaskStatusForSummary.NEVER_WORKED_UNASSIGNED.value,
461
+ TaskStatusForSummary.NEVER_WORKED_ASSIGNED.value,
462
+ TaskStatusForSummary.WORKED_NOT_REJECTED.value,
463
+ TaskStatusForSummary.WORKED_REJECTED.value,
464
+ TaskStatusForSummary.ON_HOLD.value,
465
+ TaskStatusForSummary.COMPLETE.value,
466
+ ]
467
+ df_summary = pandas.DataFrame(columns=result_columns)
468
+ else:
469
+ logger.info(f"{len(df_task)} 件のタスクを集計しました。")
470
+ df_summary = aggregate_df(df_task, metadata_keys, unit)
471
+
472
+ self.print_csv(df_summary)
473
+ logger.info(f"project_id='{project_id}' :: フェーズごとの'{unit.value}'をCSV形式で出力しました。")
474
+
475
+ def main(self) -> None:
476
+ args = self.args
477
+ project_id = args.project_id
478
+ temp_dir = Path(args.temp_dir) if args.temp_dir is not None else None
479
+
480
+ unit = AggregationUnit(args.unit)
481
+
482
+ super().validate_project(project_id, project_member_roles=[ProjectMemberRole.OWNER, ProjectMemberRole.TRAINING_DATA_USER])
483
+
484
+ # 動画時間で集計する場合は、プロジェクトが動画プロジェクトかどうかをチェック
485
+ if unit in [AggregationUnit.VIDEO_DURATION_HOUR, AggregationUnit.VIDEO_DURATION_MINUTE]:
486
+ project, _ = self.service.api.get_project(project_id)
487
+ input_data_type = project["input_data_type"]
488
+ if input_data_type != "movie":
489
+ print(f"コマンドライン引数'--unit {unit.value}' は動画プロジェクトでのみ使用できます。現在のプロジェクトの入力データタイプは'{input_data_type}'です。", file=sys.stderr) # noqa: T201
490
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
491
+
492
+ self.list_task_count_by_phase(
493
+ project_id,
494
+ temp_dir=temp_dir,
495
+ should_execute_get_tasks_api=args.execute_get_tasks_api,
496
+ not_worked_threshold_second=args.not_worked_threshold_second,
497
+ metadata_keys=args.metadata_key,
498
+ unit=unit,
499
+ )
500
+
501
+
502
+ def parse_args(parser: argparse.ArgumentParser) -> None:
503
+ argument_parser = ArgumentParser(parser)
504
+
505
+ argument_parser.add_project_id()
506
+
507
+ parser.add_argument(
508
+ "--execute_get_tasks_api",
509
+ action="store_true",
510
+ help="タスク全件ファイルをダウンロードせずに、`getTasks` APIを実行してタスク一覧を取得します。`getTasks` APIを複数回実行するので、タスク全件ファイルをダウンロードするよりも時間がかかります。",
511
+ )
512
+
513
+ parser.add_argument(
514
+ "--temp_dir",
515
+ type=str,
516
+ help="指定したディレクトリに、一時ファイルをダウンロードします。",
517
+ )
518
+
519
+ parser.add_argument(
520
+ "--not_worked_threshold_second",
521
+ type=float,
522
+ default=0,
523
+ help="作業していないとみなす作業時間の閾値を秒単位で指定します。この値以下の作業時間のタスクは、作業していないとみなします。",
524
+ )
525
+
526
+ parser.add_argument(
527
+ "--metadata_key",
528
+ type=str,
529
+ nargs="+",
530
+ help="集計対象のメタデータキーを指定します。指定したキーの値でグループ化してタスク数を集計します。",
531
+ )
532
+
533
+ parser.add_argument(
534
+ "--unit",
535
+ type=str,
536
+ choices=[e.value for e in AggregationUnit],
537
+ default=AggregationUnit.TASK.value,
538
+ help="集計の単位を指定します。task_count: タスク数、input_data_count: 入力データ数、video_duration_hour: 動画の長さ(時間)、video_duration_minute: 動画の長さ(分)。",
539
+ )
540
+
541
+ argument_parser.add_output()
542
+
543
+ parser.set_defaults(subcommand_func=main)
544
+
545
+
546
+ def main(args: argparse.Namespace) -> None:
547
+ service = build_annofabapi_resource_and_login(args)
548
+ facade = AnnofabApiFacade(service)
549
+ ListTaskCountByPhase(service, facade, args).main()
550
+
551
+
552
+ def add_parser(subparsers: argparse._SubParsersAction | None = None) -> argparse.ArgumentParser:
553
+ subcommand_name = "list_by_phase"
554
+ subcommand_help = "フェーズごとにタスク数や入力データ数などを集計し、CSV形式で出力します。"
555
+ epilog = "オーナロールまたはアノテーションユーザーロールを持つユーザで実行してください。"
556
+ parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, epilog=epilog)
557
+ parse_args(parser)
558
+ 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.1
3
+ Version: 1.114.1
4
4
  Summary: Utility Command Line Interface for AnnoFab
5
5
  Author: Kurusugawa Computer Inc.
6
6
  License: MIT
@@ -1,5 +1,5 @@
1
1
  annofabcli/__init__.py,sha256=fdBtxy5rOI8zi26jf0hmXS5KTBjQIsm2b9ZUSAIR558,319
2
- annofabcli/__main__.py,sha256=RetNbUDHGfmhdnVGhXUwPdMQ-jXLuzP0aTCu7-vLhXY,5400
2
+ annofabcli/__main__.py,sha256=w3g-2v0rcR8yZwDglX2ZK3d2fZBY_RQ0VNZHGPzoAr8,5522
3
3
  annofabcli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  annofabcli/annotation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  annofabcli/annotation/annotation_query.py,sha256=iGpbg5L6sRona3ADsBfT4lyXmyEiD5i3ujYvPhiiTbM,15681
@@ -46,7 +46,7 @@ annofabcli/annotation_zip/validate_annotation.py,sha256=aiVEMi4tzTeCKbfsnVRtytEo
46
46
  annofabcli/comment/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
47
  annofabcli/comment/delete_comment.py,sha256=4Fe6gQP2ZqgxCWg-ONCAp0-qLgFY3wA2p_WAQl3SdFQ,11324
48
48
  annofabcli/comment/download_comment_json.py,sha256=wB8U31G-7pkvN05N2erZch91bqx6oumGr9ziIXIyLuY,2392
49
- annofabcli/comment/list_all_comment.py,sha256=yBu3_FFtfnQ6VS2RUDzLKyfrP4uvn3Z25ystBCfAE58,6443
49
+ annofabcli/comment/list_all_comment.py,sha256=K6eCARgTtERTKBDgtXAPE6blrnpPhpOSR9LPMw7ZOKY,8249
50
50
  annofabcli/comment/list_comment.py,sha256=ji9n3Mx54CelTeu0k9S0F9BUrfMQSTjafNvKRNsbTk0,6153
51
51
  annofabcli/comment/put_comment.py,sha256=PB2ODE6MGTxtOeYNv66NVEaNFEAWNGG25HDBUudjJf8,11967
52
52
  annofabcli/comment/put_comment_simply.py,sha256=-t7GiBjyBSo516M_JEqFJSwUch4SXtbKBvljv2roGjw,8125
@@ -60,7 +60,7 @@ annofabcli/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
60
60
  annofabcli/common/bokeh.py,sha256=xXgWEehfFhLsLORVWNSddxOVlGwFRD1pMPEQl_4Ub1Y,1052
61
61
  annofabcli/common/cli.py,sha256=3usveVfEudPq1RLKDF6tyfwDVQ5O6hDWs34wLyl1SQI,21630
62
62
  annofabcli/common/dataclasses.py,sha256=FRDihsB-H5P5ioVE3kfJtorsS5EbSMySlHrp8BJ3ktg,385
63
- annofabcli/common/download.py,sha256=T3Catk_GzNJFY-rlSYI9YHr-00KI_bSg591e9ee5aIM,16960
63
+ annofabcli/common/download.py,sha256=8fqnaaN2W-MfLAOw5JJgVSQlyw6tt6OSAZ4Hn9COtEY,18946
64
64
  annofabcli/common/enums.py,sha256=pnMZEk8ADK2qO2Hmujx6NxeCwvSAEDNhmgK4ajPSC9Q,1233
65
65
  annofabcli/common/exceptions.py,sha256=trgC5eqvy7jgqOQ41pbAOC__uxy19GgrM9NAgkH_lBw,2051
66
66
  annofabcli/common/facade.py,sha256=P1PfZ4VW2jUUB7IWUjy-Q5aHB0rB70LvOLwVxZEfNIk,19262
@@ -89,7 +89,7 @@ annofabcli/input_data/copy_input_data.py,sha256=XWvqXJLm1zi7zhLY59mP5YpY-_n8GMf9
89
89
  annofabcli/input_data/delete_input_data.py,sha256=rG5wzudBxP2czHZpFAvdBZZUnj6m1L_N3hZOfiYkktk,8611
90
90
  annofabcli/input_data/delete_metadata_key_of_input_data.py,sha256=L-fb2pz94547AHovsxAjbrWU_83bCtpP_5xn_AhC7IE,8289
91
91
  annofabcli/input_data/download_input_data_json.py,sha256=tiofL70qcYOjr0Ugl-VunbJJlXonFi5GReLQZ5VBxmo,2856
92
- annofabcli/input_data/list_all_input_data.py,sha256=byf6Y7uwqNf2z4wbmrbMguWz5MVt7r-nqAeKWxSYWgU,9731
92
+ annofabcli/input_data/list_all_input_data.py,sha256=IqetKve6pGqgJxEbNKsqeb3B-qUJ0iU-g6BGeqxDPPc,11326
93
93
  annofabcli/input_data/list_all_input_data_merged_task.py,sha256=9t_A5TML_yxAExPgtfRYxleReOsxI-NWeVbDZmd3Gxg,12647
94
94
  annofabcli/input_data/list_input_data.py,sha256=LyncKIVwvaNBeVeKYdjF9hZ6SHntO8iy88VokxQbTH0,11327
95
95
  annofabcli/input_data/put_input_data.py,sha256=XW_rLKyAzqRPVT5xhg2ea8zDK1vl0fAfOKjTjzRyHUM,18944
@@ -205,8 +205,8 @@ annofabcli/task/copy_tasks.py,sha256=cOWtQAXqB1YBKatF2wGMMx9tqQ8RWL-AexrLBecqzqI
205
205
  annofabcli/task/delete_metadata_key_of_task.py,sha256=EywRT5LktrXzYM5H5I3EzbzbwdNOfaTFT0IOCPEmifQ,7962
206
206
  annofabcli/task/delete_tasks.py,sha256=221gB5gBNlZd4EOF0tkdIsbbsagx6fmPqavpnJwm2GI,13081
207
207
  annofabcli/task/download_task_json.py,sha256=_QJVNR2lsS7Nf5jTYOA1eaIbuXXbaKHff6on4rLGYOE,2879
208
- annofabcli/task/list_all_tasks.py,sha256=dgTn0_-f9OjQ4FYijsLc5eL8vYAK9Bb4kGraDY2ZzIs,6351
209
- annofabcli/task/list_all_tasks_added_task_history.py,sha256=w3nYCBxJ2nsxDDrZYBNqUWgPXS1qOB8NsZ9WkhISqqs,9552
208
+ annofabcli/task/list_all_tasks.py,sha256=Vmby2uqslRArVRnlAKa6vWqAzUPhwd7-pSDRIRtIWyg,7714
209
+ annofabcli/task/list_all_tasks_added_task_history.py,sha256=t2yP2sgaIgvepEErtPo8mjl8k45dbmBr_jK3YOQA01U,10314
210
210
  annofabcli/task/list_tasks.py,sha256=E4X6surGDZmAYiC7iGjLMIojZ7n2w2CJL9Ec01brXP8,10034
211
211
  annofabcli/task/list_tasks_added_task_history.py,sha256=KKXfJY-uuu5aj1vYfmFHhR_fY3YNKzF6m1UH5oA-WPA,22462
212
212
  annofabcli/task/put_tasks.py,sha256=-v6JIXX0hGjwDWTkHLnRYr60hDHRg77Y86d-dfPymoo,13641
@@ -214,18 +214,21 @@ annofabcli/task/put_tasks_by_count.py,sha256=6i7y-aH99pEDBQRWqsEQ5ESuSOL6v0SRNl3
214
214
  annofabcli/task/reject_tasks.py,sha256=lCpAfsU530O2TaYf8TbO4CeML7LoFgWwsCUCH6S_YC0,21924
215
215
  annofabcli/task/subcommand_task.py,sha256=vZIBXsHGc0203Q3yiSrQgNjov5MPSRyNLJ2qsFepVCA,2387
216
216
  annofabcli/task/update_metadata_of_task.py,sha256=Z0GF-LDt-brwgHhfz3aNYOsL4rHCf6qfzv-fzlnngOY,12817
217
+ annofabcli/task_count/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
218
+ annofabcli/task_count/list_by_phase.py,sha256=v3EZVCvh57BgbWILbadxT4luYcnXRGZ3Z08aBMN26Ek,23826
219
+ annofabcli/task_count/subcommand_task_count.py,sha256=DDys_Qa4TxJarqSkJQB3zjQipSvGA7-WDNoN2HA-_JQ,735
217
220
  annofabcli/task_history/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
218
221
  annofabcli/task_history/download_task_history_json.py,sha256=mmQ0r0jGvQyeNahVeNXX8bUjOAYxP0vgBNA9Sf0QLu8,2424
219
- annofabcli/task_history/list_all_task_history.py,sha256=pjjLKy3Jglvi5R2x5O4TFJVtSJxiJDUtLF00tSkHltY,6680
222
+ annofabcli/task_history/list_all_task_history.py,sha256=nfaMg9PoskjQRkoPr0nYEnLFfIawE1a7xr__u1TdBEM,7919
220
223
  annofabcli/task_history/list_task_history.py,sha256=PnIXNo8Tv8RUcoD0BX0-ZHzQybVIaYOhS6cAmPj82oM,6109
221
224
  annofabcli/task_history/subcommand_task_history.py,sha256=OECtQHEvheBuSUTHgDBH66UlW_T63zGU2m4Z_5B1e5Q,1017
222
225
  annofabcli/task_history_event/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
223
226
  annofabcli/task_history_event/download_task_history_event_json.py,sha256=lWyOoS2ILoPNvdQxdZ5oRKVJD783_6BtVbOwbsUGVCk,2488
224
- annofabcli/task_history_event/list_all_task_history_event.py,sha256=M8FEeZpgIb1QK01feZRHPvAAoR6K3Ztif7GICGjZoV0,6813
227
+ annofabcli/task_history_event/list_all_task_history_event.py,sha256=yNmvIEoNkfXXWPq9JZ3eAio82CrmELPSFg2M1yRHSzo,8017
225
228
  annofabcli/task_history_event/list_worktime.py,sha256=k6Hgy0pE2w5BtyUePN-LAyTbw0W2RMU4retwa6rC2uU,15497
226
229
  annofabcli/task_history_event/subcommand_task_history_event.py,sha256=dFllzpm8plnnwADwTV74h-R2LOA7rZW-xd2YnSkwTHo,1229
227
- annofabcli-1.113.1.dist-info/METADATA,sha256=Hv4z7jgkKuMPnAHF4jp-ecpL63KheapP0LR8_Gt-r0g,4949
228
- annofabcli-1.113.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
229
- annofabcli-1.113.1.dist-info/entry_points.txt,sha256=C2uSUc-kkLJpoK_mDL5FEMAdorLEMPfwSf8VBMYnIFM,56
230
- annofabcli-1.113.1.dist-info/licenses/LICENSE,sha256=pcqWYfxFtxBzhvKp3x9MXNM4xciGb2eFewaRhXUNHlo,1081
231
- annofabcli-1.113.1.dist-info/RECORD,,
230
+ annofabcli-1.114.1.dist-info/METADATA,sha256=YAxgPB0wyUWHZgC19UQm6HpQiTCnJkqzfVWe5uThbAU,4949
231
+ annofabcli-1.114.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
232
+ annofabcli-1.114.1.dist-info/entry_points.txt,sha256=C2uSUc-kkLJpoK_mDL5FEMAdorLEMPfwSf8VBMYnIFM,56
233
+ annofabcli-1.114.1.dist-info/licenses/LICENSE,sha256=pcqWYfxFtxBzhvKp3x9MXNM4xciGb2eFewaRhXUNHlo,1081
234
+ annofabcli-1.114.1.dist-info/RECORD,,