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 +2 -0
- annofabcli/comment/list_all_comment.py +41 -8
- annofabcli/common/download.py +69 -0
- annofabcli/input_data/list_all_input_data.py +34 -11
- annofabcli/task/list_all_tasks.py +29 -8
- annofabcli/task/list_all_tasks_added_task_history.py +38 -25
- annofabcli/task_count/__init__.py +0 -0
- annofabcli/task_count/list_by_phase.py +558 -0
- annofabcli/task_count/subcommand_task_count.py +21 -0
- annofabcli/task_history/list_all_task_history.py +31 -9
- annofabcli/task_history_event/list_all_task_history_event.py +30 -9
- {annofabcli-1.113.1.dist-info → annofabcli-1.114.1.dist-info}/METADATA +1 -1
- {annofabcli-1.113.1.dist-info → annofabcli-1.114.1.dist-info}/RECORD +16 -13
- {annofabcli-1.113.1.dist-info → annofabcli-1.114.1.dist-info}/WHEEL +0 -0
- {annofabcli-1.113.1.dist-info → annofabcli-1.114.1.dist-info}/entry_points.txt +0 -0
- {annofabcli-1.113.1.dist-info → annofabcli-1.114.1.dist-info}/licenses/LICENSE +0 -0
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
|
-
|
|
48
|
-
json_path =
|
|
49
|
-
|
|
50
|
-
with
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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,
|
annofabcli/common/download.py
CHANGED
|
@@ -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
|
-
|
|
80
|
-
json_path =
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
56
|
-
json_path =
|
|
57
|
-
|
|
58
|
-
with
|
|
59
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
with
|
|
87
|
-
|
|
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
|
-
|
|
49
|
-
tmp_json_path =
|
|
50
|
-
|
|
51
|
-
with
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
52
|
-
tmp_json_file =
|
|
53
|
-
|
|
54
|
-
with
|
|
55
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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,5 +1,5 @@
|
|
|
1
1
|
annofabcli/__init__.py,sha256=fdBtxy5rOI8zi26jf0hmXS5KTBjQIsm2b9ZUSAIR558,319
|
|
2
|
-
annofabcli/__main__.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
209
|
-
annofabcli/task/list_all_tasks_added_task_history.py,sha256=
|
|
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=
|
|
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=
|
|
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.
|
|
228
|
-
annofabcli-1.
|
|
229
|
-
annofabcli-1.
|
|
230
|
-
annofabcli-1.
|
|
231
|
-
annofabcli-1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|