annofabcli 1.104.0__py3-none-any.whl → 1.105.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- annofabcli/annotation/annotation_query.py +5 -0
- annofabcli/annotation/change_annotation_attributes_per_annotation.py +277 -0
- annofabcli/annotation/delete_annotation.py +1 -2
- annofabcli/annotation/dump_annotation.py +31 -13
- annofabcli/annotation/restore_annotation.py +45 -5
- annofabcli/annotation/subcommand_annotation.py +2 -0
- annofabcli/comment/put_comment.py +1 -1
- annofabcli/common/visualize.py +3 -6
- annofabcli/statistics/list_annotation_area.py +3 -0
- annofabcli/statistics/list_annotation_attribute.py +3 -0
- annofabcli/statistics/list_annotation_attribute_filled_count.py +8 -0
- annofabcli/statistics/list_annotation_count.py +12 -0
- annofabcli/statistics/list_annotation_duration.py +6 -0
- annofabcli/statistics/list_video_duration.py +2 -1
- annofabcli/task_history_event/list_all_task_history_event.py +25 -7
- annofabcli/task_history_event/list_worktime.py +71 -12
- {annofabcli-1.104.0.dist-info → annofabcli-1.105.0.dist-info}/METADATA +1 -1
- {annofabcli-1.104.0.dist-info → annofabcli-1.105.0.dist-info}/RECORD +21 -20
- {annofabcli-1.104.0.dist-info → annofabcli-1.105.0.dist-info}/WHEEL +0 -0
- {annofabcli-1.104.0.dist-info → annofabcli-1.105.0.dist-info}/entry_points.txt +0 -0
- {annofabcli-1.104.0.dist-info → annofabcli-1.105.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -105,6 +105,11 @@ def _get_additional_data_v2(additional_data: dict[str, Any], attribute_value: At
|
|
|
105
105
|
return get_english_message(additional_data["name"])
|
|
106
106
|
|
|
107
107
|
additional_data_definition_id = additional_data["additional_data_definition_id"]
|
|
108
|
+
if attribute_value is None:
|
|
109
|
+
return {
|
|
110
|
+
"definition_id": additional_data_definition_id,
|
|
111
|
+
"value": None,
|
|
112
|
+
}
|
|
108
113
|
|
|
109
114
|
additional_data_type: str = additional_data["type"]
|
|
110
115
|
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Optional, Union
|
|
10
|
+
|
|
11
|
+
import annofabapi
|
|
12
|
+
from annofabapi.models import ProjectMemberRole
|
|
13
|
+
from annofabapi.pydantic_models.task_status import TaskStatus
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
|
|
16
|
+
import annofabcli
|
|
17
|
+
from annofabcli.annotation.annotation_query import convert_attributes_from_cli_to_additional_data_list_v2
|
|
18
|
+
from annofabcli.annotation.dump_annotation import DumpAnnotationMain
|
|
19
|
+
from annofabcli.common.cli import (
|
|
20
|
+
COMMAND_LINE_ERROR_STATUS_CODE,
|
|
21
|
+
ArgumentParser,
|
|
22
|
+
CommandLine,
|
|
23
|
+
CommandLineWithConfirm,
|
|
24
|
+
build_annofabapi_resource_and_login,
|
|
25
|
+
get_json_from_args,
|
|
26
|
+
)
|
|
27
|
+
from annofabcli.common.facade import AnnofabApiFacade
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
Attributes = dict[str, Optional[Union[str, int, bool]]]
|
|
33
|
+
"""属性情報"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TargetAnnotation(BaseModel):
|
|
37
|
+
task_id: str
|
|
38
|
+
input_data_id: str
|
|
39
|
+
annotation_id: str
|
|
40
|
+
attributes: Attributes
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_annotation_list_per_task_id_input_data_id(anno_list: list[TargetAnnotation]) -> dict[str, dict[str, list[TargetAnnotation]]]:
|
|
44
|
+
"""
|
|
45
|
+
タスクIDと入力データIDごとにアノテーションをグループ化する。
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
anno_list: アノテーションのリスト
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
タスクIDと入力データIDをキーとしたアノテーションの辞書
|
|
52
|
+
"""
|
|
53
|
+
grouped: dict[str, dict[str, list[TargetAnnotation]]] = defaultdict(lambda: defaultdict(list))
|
|
54
|
+
|
|
55
|
+
for annotation in anno_list:
|
|
56
|
+
grouped[annotation.task_id][annotation.input_data_id].append(annotation)
|
|
57
|
+
return grouped
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ChangeAnnotationAttributesPerAnnotationMain(CommandLineWithConfirm):
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
service: annofabapi.Resource,
|
|
64
|
+
*,
|
|
65
|
+
project_id: str,
|
|
66
|
+
is_force: bool,
|
|
67
|
+
all_yes: bool,
|
|
68
|
+
backup_dir: Optional[Path] = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
self.service = service
|
|
71
|
+
self.project_id = project_id
|
|
72
|
+
self.is_force = is_force
|
|
73
|
+
self.backup_dir = backup_dir
|
|
74
|
+
self.annotation_specs, _ = self.service.api.get_annotation_specs(project_id, query_params={"v": "3"})
|
|
75
|
+
self.dump_annotation_obj = DumpAnnotationMain(service, project_id)
|
|
76
|
+
super().__init__(all_yes)
|
|
77
|
+
|
|
78
|
+
def change_annotation_attributes_by_frame(self, task_id: str, input_data_id: str, anno_list: list[TargetAnnotation]) -> bool:
|
|
79
|
+
"""
|
|
80
|
+
フレームごとにアノテーション属性値を変更する。
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
task_id: タスクID
|
|
84
|
+
input_data_id: 入力データID
|
|
85
|
+
additional_data_list: 変更後の属性値(`AdditionalDataListV2`スキーマ)
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
editor_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id=task_id, input_data_id=input_data_id, query_params={"v": "2"})
|
|
89
|
+
|
|
90
|
+
if self.backup_dir is not None:
|
|
91
|
+
(self.backup_dir / task_id).mkdir(exist_ok=True, parents=True)
|
|
92
|
+
self.dump_annotation_obj.dump_editor_annotation(editor_annotation, json_path=self.backup_dir / task_id / f"{input_data_id}.json")
|
|
93
|
+
|
|
94
|
+
details_map = {detail["annotation_id"]: detail for detail in editor_annotation["details"]}
|
|
95
|
+
|
|
96
|
+
def _to_request_body_elm(anno: TargetAnnotation) -> dict[str, Any]:
|
|
97
|
+
additional_data_list = convert_attributes_from_cli_to_additional_data_list_v2(anno.attributes, annotation_specs=self.annotation_specs)
|
|
98
|
+
return {
|
|
99
|
+
"data": {
|
|
100
|
+
"project_id": editor_annotation["project_id"],
|
|
101
|
+
"task_id": editor_annotation["task_id"],
|
|
102
|
+
"input_data_id": editor_annotation["input_data_id"],
|
|
103
|
+
"updated_datetime": editor_annotation["updated_datetime"],
|
|
104
|
+
"annotation_id": anno.annotation_id,
|
|
105
|
+
"label_id": details_map[anno.annotation_id]["label_id"],
|
|
106
|
+
"additional_data_list": additional_data_list,
|
|
107
|
+
},
|
|
108
|
+
"_type": "PutV2",
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
request_body = [_to_request_body_elm(annotation) for annotation in anno_list]
|
|
112
|
+
|
|
113
|
+
self.service.api.batch_update_annotations(self.project_id, request_body=request_body)
|
|
114
|
+
logger.debug(f"task_id='{task_id}', input_data_id='{input_data_id}' :: {len(request_body)}件の属性値を変更しました。")
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
def change_annotation_attributes_for_task(self, task_id: str, annotation_list_per_input_data_id: dict[str, list[TargetAnnotation]]) -> tuple[bool, int, int]:
|
|
118
|
+
"""
|
|
119
|
+
1個のタスクに含まれるアノテーションの属性値を変更する。
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
task_id: タスクID
|
|
123
|
+
annotation_list_per_input_data_id: 入力データIDごとのアノテーションリスト
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
tuple:
|
|
127
|
+
[0]: 属性値の変更可能なタスクかどうか
|
|
128
|
+
[1]: 属性値の変更に成功したアノテーション数
|
|
129
|
+
[2]: 属性値を変更できなかったアノテーション数
|
|
130
|
+
|
|
131
|
+
"""
|
|
132
|
+
annotation_count = sum(len(v) for v in annotation_list_per_input_data_id.values())
|
|
133
|
+
task = self.service.wrapper.get_task_or_none(self.project_id, task_id)
|
|
134
|
+
|
|
135
|
+
succeed_to_change_annotation_count = 0
|
|
136
|
+
failed_to_change_annotation_count = 0
|
|
137
|
+
|
|
138
|
+
if task is None:
|
|
139
|
+
logger.warning(f"task_id='{task_id}' :: タスクが存在しないため、{annotation_count} 件のアノテーションの属性値の変更をスキップします。")
|
|
140
|
+
return False, 0, annotation_count
|
|
141
|
+
|
|
142
|
+
if task["status"] == TaskStatus.WORKING.value:
|
|
143
|
+
logger.info(f"task_id='{task_id}' :: タスクが作業中状態のため、{annotation_count} 件のアノテーションの属性値の変更をスキップします。")
|
|
144
|
+
failed_to_change_annotation_count += annotation_count
|
|
145
|
+
return False, 0, annotation_count
|
|
146
|
+
|
|
147
|
+
if not self.is_force: # noqa: SIM102
|
|
148
|
+
if task["status"] == TaskStatus.COMPLETE.value:
|
|
149
|
+
logger.info(
|
|
150
|
+
f"task_id='{task_id}' :: タスクが完了状態のため、アノテーション {annotation_count} 件のアノテーションの属性値の変更をスキップします。"
|
|
151
|
+
f"完了状態のタスクのアノテーションを削除するには、`--force`オプションを指定してください。"
|
|
152
|
+
)
|
|
153
|
+
failed_to_change_annotation_count += annotation_count
|
|
154
|
+
return False, 0, annotation_count
|
|
155
|
+
|
|
156
|
+
if not self.confirm_processing(f"task_id='{task_id}'に含まれるアノテーション{annotation_count}件の属性値を変更しますか?"):
|
|
157
|
+
return False, 0, annotation_count
|
|
158
|
+
|
|
159
|
+
for input_data_id, sub_anno_list in annotation_list_per_input_data_id.items():
|
|
160
|
+
try:
|
|
161
|
+
if self.change_annotation_attributes_by_frame(task_id, input_data_id, sub_anno_list):
|
|
162
|
+
succeed_to_change_annotation_count += len(sub_anno_list)
|
|
163
|
+
else:
|
|
164
|
+
failed_to_change_annotation_count += len(sub_anno_list)
|
|
165
|
+
except Exception:
|
|
166
|
+
logger.warning(f"task_id='{task_id}', input_data_id='{input_data_id}' :: アノテーションの属性値変更に失敗しました。", exc_info=True)
|
|
167
|
+
failed_to_change_annotation_count += len(sub_anno_list)
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
return True, succeed_to_change_annotation_count, failed_to_change_annotation_count
|
|
171
|
+
|
|
172
|
+
def change_annotation_attributes(self, anno_list: list[TargetAnnotation]) -> None:
|
|
173
|
+
"""
|
|
174
|
+
アノテーションごとに属性値を変更する。
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
anno_list: 各アノテーションの変更内容リスト
|
|
178
|
+
"""
|
|
179
|
+
changed_task_count = 0
|
|
180
|
+
failed_to_change_annotation_count = 0
|
|
181
|
+
annotation_list_per_task_id_input_data_id = get_annotation_list_per_task_id_input_data_id(anno_list)
|
|
182
|
+
|
|
183
|
+
total_task_count = len(annotation_list_per_task_id_input_data_id)
|
|
184
|
+
total_annotation_count = len(anno_list)
|
|
185
|
+
|
|
186
|
+
for task_id, input_data_dict in annotation_list_per_task_id_input_data_id.items():
|
|
187
|
+
is_changeable_task, succeed_to_change_annotation_count, failed_to_change_annotation_count = self.change_annotation_attributes_for_task(task_id, input_data_dict)
|
|
188
|
+
if is_changeable_task:
|
|
189
|
+
changed_task_count += 1
|
|
190
|
+
|
|
191
|
+
logger.info(
|
|
192
|
+
f"{succeed_to_change_annotation_count}/{total_annotation_count} 件のアノテーションの属性値を変更しました。 :: "
|
|
193
|
+
f"アノテーションが変更されたタスク数は {changed_task_count}/{total_task_count} 件です。"
|
|
194
|
+
f"{failed_to_change_annotation_count} 件のアノテーションは変更できませんでした。"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class ChangeAttributesPerAnnotation(CommandLine):
|
|
199
|
+
"""
|
|
200
|
+
アノテーションごとに属性値を個別変更
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
COMMON_MESSAGE = "annofabcli annotation change_attributes_per_annotation: error:"
|
|
204
|
+
|
|
205
|
+
def main(self) -> None:
|
|
206
|
+
args = self.args
|
|
207
|
+
|
|
208
|
+
annotation_items = get_json_from_args(args.json)
|
|
209
|
+
if not isinstance(annotation_items, list):
|
|
210
|
+
print(f"{self.COMMON_MESSAGE} argument --json: JSON形式が不正です。オブジェクトの配列を指定してください。", file=sys.stderr) # noqa: T201
|
|
211
|
+
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
212
|
+
|
|
213
|
+
target_annotation_list = [TargetAnnotation.model_validate(anno) for anno in annotation_items]
|
|
214
|
+
|
|
215
|
+
project_id = args.project_id
|
|
216
|
+
|
|
217
|
+
if args.backup is None:
|
|
218
|
+
print( # noqa: T201
|
|
219
|
+
"間違えてアノテーションを変更してしまっときに復元できるようにするため、'--backup'でバックアップ用のディレクトリを指定することを推奨します。",
|
|
220
|
+
file=sys.stderr,
|
|
221
|
+
)
|
|
222
|
+
if not self.confirm_processing("復元用のバックアップディレクトリが指定されていません。処理を続行しますか?"):
|
|
223
|
+
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
224
|
+
backup_dir = None
|
|
225
|
+
else:
|
|
226
|
+
backup_dir = Path(args.backup)
|
|
227
|
+
|
|
228
|
+
# プロジェクト権限チェック
|
|
229
|
+
super().validate_project(project_id, [ProjectMemberRole.OWNER, ProjectMemberRole.ACCEPTER])
|
|
230
|
+
|
|
231
|
+
main_obj = ChangeAnnotationAttributesPerAnnotationMain(self.service, project_id=project_id, all_yes=args.yes, is_force=args.force, backup_dir=backup_dir)
|
|
232
|
+
main_obj.change_annotation_attributes(target_annotation_list)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def main(args: argparse.Namespace) -> None:
|
|
236
|
+
service = build_annofabapi_resource_and_login(args)
|
|
237
|
+
facade = AnnofabApiFacade(service)
|
|
238
|
+
ChangeAttributesPerAnnotation(service, facade, args).main()
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
242
|
+
argument_parser = ArgumentParser(parser)
|
|
243
|
+
argument_parser.add_project_id()
|
|
244
|
+
|
|
245
|
+
sample_json_obj = [{"task_id": "t1", "input_data_id": "i1", "annotation_id": "a1", "attributes": {"occluded": True}}]
|
|
246
|
+
parser.add_argument(
|
|
247
|
+
"--json",
|
|
248
|
+
type=str,
|
|
249
|
+
required=True,
|
|
250
|
+
help="各アノテーションごとに変更内容を記載したJSONリストを指定します。 ``file://`` を先頭に付けるとJSON形式のファイルを指定できます。\n"
|
|
251
|
+
f"(例) '{json.dumps(sample_json_obj, ensure_ascii=False)}'",
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
parser.add_argument(
|
|
255
|
+
"--force",
|
|
256
|
+
action="store_true",
|
|
257
|
+
help="指定した場合は、完了状態のタスクのアノテーションも属性値を変更します。ただし、完了状態のタスクのアノテーションを変更するには、オーナーロールを持つユーザーが実行する必要があります。",
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
parser.add_argument(
|
|
261
|
+
"--backup",
|
|
262
|
+
type=Path,
|
|
263
|
+
required=False,
|
|
264
|
+
help="アノテーションのバックアップを保存するディレクトリのパス。アノテーションの復元は ``annotation restore`` コマンドで実現できます。",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
parser.set_defaults(subcommand_func=main)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
|
|
271
|
+
subcommand_name = "change_attributes_per_annotation"
|
|
272
|
+
subcommand_help = "各アノテーションの属性値を変更します。"
|
|
273
|
+
epilog = "オーナロールまたはチェッカーロールを持つユーザで実行してください。"
|
|
274
|
+
|
|
275
|
+
parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, epilog=epilog)
|
|
276
|
+
parse_args(parser)
|
|
277
|
+
return parser
|
|
@@ -296,8 +296,7 @@ class DeleteAnnotationMain(CommandLineWithConfirm):
|
|
|
296
296
|
deleted_task_count += 1
|
|
297
297
|
for input_data_id, annotation_ids in sub_grouped.items():
|
|
298
298
|
# 指定input_data_idの全annotationを取得
|
|
299
|
-
|
|
300
|
-
editor_annotation = self.service.wrapper.get_editor_annotation_or_none(self.project_id, task_id=task_id, input_data_id=input_data_id, query_params={"v": "1"})
|
|
299
|
+
editor_annotation = self.service.wrapper.get_editor_annotation_or_none(self.project_id, task_id=task_id, input_data_id=input_data_id, query_params={"v": "2"})
|
|
301
300
|
if editor_annotation is None:
|
|
302
301
|
logger.warning(
|
|
303
302
|
f"task_id='{task_id}'のタスクに、input_data_id='{input_data_id}'の入力データが含まれていません。 アノテーションの削除をスキップします。 :: annotation_ids={annotation_ids}"
|
|
@@ -27,26 +27,44 @@ class DumpAnnotationMain:
|
|
|
27
27
|
def dump_editor_annotation(self, editor_annotation: dict[str, Any], json_path: Path) -> None:
|
|
28
28
|
"""
|
|
29
29
|
`getEditorAnnotation` APIのレスポンスをファイルに保存する。
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
editor_annotation: v1, v2 のどちらの形式でも対応。
|
|
30
33
|
"""
|
|
31
34
|
json_path.write_text(json.dumps(editor_annotation, ensure_ascii=False), encoding="utf-8")
|
|
32
|
-
|
|
33
35
|
details = editor_annotation["details"]
|
|
34
|
-
outer_details = [e for e in details if e["data_holding_type"] == AnnotationDataHoldingType.OUTER.value]
|
|
35
|
-
if len(outer_details) == 0:
|
|
36
|
-
return
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
if editor_annotation.get("format_version") == "2.0.0":
|
|
38
|
+
outer_details = [e for e in details if e["body"]["_type"] == "Outer"]
|
|
39
|
+
if len(outer_details) == 0:
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
input_data_id = editor_annotation["input_data_id"]
|
|
43
|
+
outer_dir = json_path.parent / input_data_id
|
|
44
|
+
outer_dir.mkdir(exist_ok=True, parents=True)
|
|
45
|
+
# 塗りつぶし画像など外部リソースに保存されているファイルをダウンロードする
|
|
46
|
+
for detail in outer_details:
|
|
47
|
+
annotation_id = detail["annotation_id"]
|
|
48
|
+
outer_file_path = outer_dir / f"{annotation_id}"
|
|
49
|
+
self.service.wrapper.download(detail["body"]["url"], outer_file_path)
|
|
50
|
+
|
|
51
|
+
else:
|
|
52
|
+
outer_details = [e for e in details if e["data_holding_type"] == AnnotationDataHoldingType.OUTER.value]
|
|
53
|
+
if len(outer_details) == 0:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
input_data_id = editor_annotation["input_data_id"]
|
|
57
|
+
outer_dir = json_path.parent / input_data_id
|
|
58
|
+
outer_dir.mkdir(exist_ok=True, parents=True)
|
|
41
59
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
60
|
+
# 塗りつぶし画像など外部リソースに保存されているファイルをダウンロードする
|
|
61
|
+
for detail in outer_details:
|
|
62
|
+
annotation_id = detail["annotation_id"]
|
|
63
|
+
outer_file_path = outer_dir / f"{annotation_id}"
|
|
64
|
+
self.service.wrapper.download(detail["url"], outer_file_path)
|
|
47
65
|
|
|
48
66
|
def dump_annotation_for_input_data(self, task_id: str, input_data_id: str, task_dir: Path) -> None:
|
|
49
|
-
editor_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id)
|
|
67
|
+
editor_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id, query_params={"v": "2"})
|
|
50
68
|
json_path = task_dir / f"{input_data_id}.json"
|
|
51
69
|
self.dump_editor_annotation(editor_annotation=editor_annotation, json_path=json_path)
|
|
52
70
|
|
|
@@ -74,10 +74,17 @@ class RestoreAnnotationMain(CommandLineWithConfirm):
|
|
|
74
74
|
|
|
75
75
|
return detail
|
|
76
76
|
|
|
77
|
-
def
|
|
77
|
+
def editor_annotation_to_request_body_v1(self, editor_annotation: dict[str, Any], parser: SimpleAnnotationParser) -> dict[str, Any]:
|
|
78
|
+
"""
|
|
79
|
+
`get_editor_annotation`で取得したアノテーション(v1)を、`put_annotation` APIに渡すリクエストボディ(v1)に変換する。
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
editor_annotation: `get_editor_annotation`で取得したアノテーション(v1)
|
|
83
|
+
parser: SimpleAnnotationParserインスタンス。アノテーションのファイルを開くために利用する。
|
|
84
|
+
"""
|
|
78
85
|
# infer_missing=Trueを指定する理由:Optional型のキーが存在しない場合でも、AnnotationV1データクラスのインスタンスを生成できるようにするため
|
|
79
86
|
# https://qiita.com/yuji38kwmt/items/c5b56f70da3b8a70ba31
|
|
80
|
-
annotation: AnnotationV1 = AnnotationV1.from_dict(
|
|
87
|
+
annotation: AnnotationV1 = AnnotationV1.from_dict(editor_annotation, infer_missing=True)
|
|
81
88
|
request_details: list[dict[str, Any]] = []
|
|
82
89
|
for detail in annotation.details:
|
|
83
90
|
request_detail = self._to_annotation_detail_for_request(parser, detail)
|
|
@@ -94,15 +101,48 @@ class RestoreAnnotationMain(CommandLineWithConfirm):
|
|
|
94
101
|
|
|
95
102
|
return request_body
|
|
96
103
|
|
|
104
|
+
def editor_annotation_to_request_body_v2(self, editor_annotation: dict[str, Any], parser: SimpleAnnotationParser) -> dict[str, Any]:
|
|
105
|
+
"""
|
|
106
|
+
`get_editor_annotation`で取得したアノテーション(v2)を、`put_annotation` APIに渡すリクエストボディ(v2)に変換する。
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
editor_annotation: `get_editor_annotation`で取得したアノテーション(v2)
|
|
110
|
+
parser: SimpleAnnotationParserインスタンス。アノテーションのファイルを開くために利用する。
|
|
111
|
+
"""
|
|
112
|
+
request_details: list[dict[str, Any]] = []
|
|
113
|
+
for detail in editor_annotation["details"]:
|
|
114
|
+
new_detail = copy.deepcopy(detail)
|
|
115
|
+
new_detail["_type"] = "Import"
|
|
116
|
+
annotation_id = detail["annotation_id"]
|
|
117
|
+
detail_body_type = detail["body"]["_type"]
|
|
118
|
+
if detail_body_type == "Outer":
|
|
119
|
+
with parser.open_outer_file(annotation_id) as f:
|
|
120
|
+
# TODO content typeを確認
|
|
121
|
+
s3_path = self.service.wrapper.upload_data_to_s3(self.project_id, f, content_type="application/octet-stream")
|
|
122
|
+
new_detail["body"] = {"path": s3_path, "_type": "Outer"}
|
|
123
|
+
request_details.append(new_detail)
|
|
124
|
+
elif detail_body_type == "Inner":
|
|
125
|
+
request_details.append(new_detail)
|
|
126
|
+
else:
|
|
127
|
+
raise ValueError(f"detail_body_type がサポート対象外です。:: detail={detail}")
|
|
128
|
+
|
|
129
|
+
request_body = {"project_id": self.project_id, "task_id": parser.task_id, "input_data_id": parser.input_data_id, "details": request_details, "format_version": "2.0.0"}
|
|
130
|
+
|
|
131
|
+
return request_body
|
|
132
|
+
|
|
97
133
|
def put_annotation_for_input_data(self, parser: SimpleAnnotationParser) -> bool:
|
|
98
134
|
task_id = parser.task_id
|
|
99
135
|
input_data_id = parser.input_data_id
|
|
100
136
|
|
|
101
|
-
old_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id)
|
|
102
|
-
|
|
103
137
|
logger.info(f"task_id='{task_id}', input_data_id='{input_data_id}' :: アノテーションをリストアします。")
|
|
104
|
-
request_body = self.parser_to_request_body(parser)
|
|
105
138
|
|
|
139
|
+
editor_annotation = parser.load_json()
|
|
140
|
+
if editor_annotation.get("format_version") == "2.0.0":
|
|
141
|
+
request_body = self.editor_annotation_to_request_body_v2(editor_annotation, parser)
|
|
142
|
+
else:
|
|
143
|
+
request_body = self.editor_annotation_to_request_body_v1(editor_annotation, parser)
|
|
144
|
+
|
|
145
|
+
old_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id, query_params={"v": "2"})
|
|
106
146
|
updated_datetime = old_annotation["updated_datetime"] if old_annotation is not None else None
|
|
107
147
|
request_body["updated_datetime"] = updated_datetime
|
|
108
148
|
self.service.api.put_annotation(self.project_id, task_id, input_data_id, request_body=request_body)
|
|
@@ -2,6 +2,7 @@ import argparse
|
|
|
2
2
|
from typing import Optional
|
|
3
3
|
|
|
4
4
|
import annofabcli.annotation.change_annotation_attributes
|
|
5
|
+
import annofabcli.annotation.change_annotation_attributes_per_annotation
|
|
5
6
|
import annofabcli.annotation.change_annotation_properties
|
|
6
7
|
import annofabcli.annotation.copy_annotation
|
|
7
8
|
import annofabcli.annotation.delete_annotation
|
|
@@ -21,6 +22,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
21
22
|
|
|
22
23
|
# サブコマンドの定義
|
|
23
24
|
annofabcli.annotation.change_annotation_attributes.add_parser(subparsers)
|
|
25
|
+
annofabcli.annotation.change_annotation_attributes_per_annotation.add_parser(subparsers)
|
|
24
26
|
annofabcli.annotation.change_annotation_properties.add_parser(subparsers)
|
|
25
27
|
annofabcli.annotation.copy_annotation.add_parser(subparsers)
|
|
26
28
|
annofabcli.annotation.delete_annotation.add_parser(subparsers)
|
|
@@ -67,7 +67,7 @@ class PutCommentMain(CommandLineWithConfirm):
|
|
|
67
67
|
"""batch_update_comments に渡すリクエストボディを作成する。"""
|
|
68
68
|
|
|
69
69
|
def _create_dict_annotation_id() -> dict[str, str]:
|
|
70
|
-
content, _ = self.service.api.get_editor_annotation(self.project_id, task["task_id"], input_data_id)
|
|
70
|
+
content, _ = self.service.api.get_editor_annotation(self.project_id, task["task_id"], input_data_id, query_params={"v": "2"})
|
|
71
71
|
details = content["details"]
|
|
72
72
|
return {e["annotation_id"]: e["label_id"] for e in details}
|
|
73
73
|
|
annofabcli/common/visualize.py
CHANGED
|
@@ -375,13 +375,10 @@ class AddProps:
|
|
|
375
375
|
|
|
376
376
|
* user_id
|
|
377
377
|
* username
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
task:
|
|
381
|
-
|
|
382
|
-
Returns:
|
|
383
|
-
Task情報
|
|
378
|
+
* request.user_id
|
|
379
|
+
* request.username
|
|
384
380
|
|
|
385
381
|
"""
|
|
386
382
|
self._add_user_info(task_history_event)
|
|
383
|
+
self._add_user_info(task_history_event["request"])
|
|
387
384
|
return task_history_event
|
|
@@ -55,6 +55,7 @@ def lazy_parse_simple_annotation_by_input_data(annotation_path: Path) -> Iterato
|
|
|
55
55
|
|
|
56
56
|
@dataclass(frozen=True)
|
|
57
57
|
class AnnotationAreaInfo(DataClassJsonMixin):
|
|
58
|
+
project_id: str
|
|
58
59
|
task_id: str
|
|
59
60
|
task_status: str
|
|
60
61
|
task_phase: str
|
|
@@ -92,6 +93,7 @@ def get_annotation_area_info_list(parser: SimpleAnnotationParser, simple_annotat
|
|
|
92
93
|
|
|
93
94
|
result.append(
|
|
94
95
|
AnnotationAreaInfo(
|
|
96
|
+
project_id=simple_annotation["project_id"],
|
|
95
97
|
task_id=simple_annotation["task_id"],
|
|
96
98
|
task_phase=simple_annotation["task_phase"],
|
|
97
99
|
task_phase_stage=simple_annotation["task_phase_stage"],
|
|
@@ -135,6 +137,7 @@ def create_df(
|
|
|
135
137
|
annotation_area_list: list[AnnotationAreaInfo],
|
|
136
138
|
) -> pandas.DataFrame:
|
|
137
139
|
columns = [
|
|
140
|
+
"project_id",
|
|
138
141
|
"task_id",
|
|
139
142
|
"task_status",
|
|
140
143
|
"task_phase",
|
|
@@ -57,6 +57,7 @@ class AnnotationAttribute(pydantic.BaseModel):
|
|
|
57
57
|
入力データまたはタスク単位の区間アノテーションの長さ情報。
|
|
58
58
|
"""
|
|
59
59
|
|
|
60
|
+
project_id: str
|
|
60
61
|
task_id: str
|
|
61
62
|
task_status: str
|
|
62
63
|
task_phase: str
|
|
@@ -87,6 +88,7 @@ def get_annotation_attribute_list_from_annotation_json(simple_annotation: dict[s
|
|
|
87
88
|
|
|
88
89
|
result.append(
|
|
89
90
|
AnnotationAttribute(
|
|
91
|
+
project_id=simple_annotation["project_id"],
|
|
90
92
|
task_id=simple_annotation["task_id"],
|
|
91
93
|
task_status=simple_annotation["task_status"],
|
|
92
94
|
task_phase=simple_annotation["task_phase"],
|
|
@@ -141,6 +143,7 @@ def print_annotation_attribute_list_as_csv(annotation_attribute_list: list, outp
|
|
|
141
143
|
df = pandas.json_normalize(annotation_attribute_list)
|
|
142
144
|
|
|
143
145
|
base_columns = [
|
|
146
|
+
"project_id",
|
|
144
147
|
"task_id",
|
|
145
148
|
"task_status",
|
|
146
149
|
"task_phase",
|
|
@@ -100,6 +100,7 @@ class AnnotationCountByInputData(DataClassJsonMixin, HasAnnotationAttributeCount
|
|
|
100
100
|
入力データ単位のアノテーション数の情報。
|
|
101
101
|
"""
|
|
102
102
|
|
|
103
|
+
project_id: str
|
|
103
104
|
task_id: str
|
|
104
105
|
task_status: TaskStatus
|
|
105
106
|
task_phase: TaskPhase
|
|
@@ -126,6 +127,7 @@ class AnnotationCountByTask(DataClassJsonMixin, HasAnnotationAttributeCounts):
|
|
|
126
127
|
タスク単位のアノテーション数の情報。
|
|
127
128
|
"""
|
|
128
129
|
|
|
130
|
+
project_id: str
|
|
129
131
|
task_id: str
|
|
130
132
|
task_status: TaskStatus
|
|
131
133
|
task_phase: TaskPhase
|
|
@@ -174,6 +176,7 @@ def convert_annotation_count_list_by_input_data_to_by_task(annotation_count_list
|
|
|
174
176
|
|
|
175
177
|
result.append(
|
|
176
178
|
AnnotationCountByTask(
|
|
179
|
+
project_id=first_elm.project_id,
|
|
177
180
|
task_id=task_id,
|
|
178
181
|
task_status=first_elm.task_status,
|
|
179
182
|
task_phase=first_elm.task_phase,
|
|
@@ -263,6 +266,7 @@ class ListAnnotationCounterByInputData:
|
|
|
263
266
|
frame_no = self.frame_no_map.get((simple_annotation["task_id"], simple_annotation["input_data_id"]))
|
|
264
267
|
|
|
265
268
|
return AnnotationCountByInputData(
|
|
269
|
+
project_id=simple_annotation["project_id"],
|
|
266
270
|
task_id=simple_annotation["task_id"],
|
|
267
271
|
task_phase=TaskPhase(simple_annotation["task_phase"]),
|
|
268
272
|
task_phase_stage=simple_annotation["task_phase_stage"],
|
|
@@ -343,6 +347,7 @@ class AnnotationCountCsvByAttribute:
|
|
|
343
347
|
prior_attribute_columns: Optional[list[tuple[str, str, str]]] = None,
|
|
344
348
|
) -> list[tuple[str, str, str]]:
|
|
345
349
|
basic_columns = [
|
|
350
|
+
("project_id", "", ""),
|
|
346
351
|
("task_id", "", ""),
|
|
347
352
|
("task_status", "", ""),
|
|
348
353
|
("task_phase", "", ""),
|
|
@@ -360,6 +365,7 @@ class AnnotationCountCsvByAttribute:
|
|
|
360
365
|
prior_attribute_columns: Optional[list[tuple[str, str, str]]] = None,
|
|
361
366
|
) -> list[tuple[str, str, str]]:
|
|
362
367
|
basic_columns = [
|
|
368
|
+
("project_id", "", ""),
|
|
363
369
|
("task_id", "", ""),
|
|
364
370
|
("task_status", "", ""),
|
|
365
371
|
("task_phase", "", ""),
|
|
@@ -377,6 +383,7 @@ class AnnotationCountCsvByAttribute:
|
|
|
377
383
|
) -> pandas.DataFrame:
|
|
378
384
|
def to_cell(c: AnnotationCountByInputData) -> dict[tuple[str, str, str], Any]:
|
|
379
385
|
cell: dict[tuple[str, str, str], Any] = {
|
|
386
|
+
("project_id", "", ""): c.project_id,
|
|
380
387
|
("task_id", "", ""): c.task_id,
|
|
381
388
|
("task_status", "", ""): c.task_status.value,
|
|
382
389
|
("task_phase", "", ""): c.task_phase.value,
|
|
@@ -405,6 +412,7 @@ class AnnotationCountCsvByAttribute:
|
|
|
405
412
|
) -> pandas.DataFrame:
|
|
406
413
|
def to_cell(c: AnnotationCountByTask) -> dict[tuple[str, str, str], Any]:
|
|
407
414
|
cell: dict[tuple[str, str, str], Any] = {
|
|
415
|
+
("project_id", "", ""): c.project_id,
|
|
408
416
|
("task_id", "", ""): c.task_id,
|
|
409
417
|
("task_status", "", ""): c.task_status.value,
|
|
410
418
|
("task_phase", "", ""): c.task_phase.value,
|
|
@@ -115,6 +115,7 @@ class AnnotationCounter(abc.ABC):
|
|
|
115
115
|
|
|
116
116
|
@dataclass(frozen=True)
|
|
117
117
|
class AnnotationCounterByTask(AnnotationCounter, DataClassJsonMixin):
|
|
118
|
+
project_id: str
|
|
118
119
|
task_id: str
|
|
119
120
|
task_status: TaskStatus
|
|
120
121
|
task_phase: TaskPhase
|
|
@@ -124,6 +125,7 @@ class AnnotationCounterByTask(AnnotationCounter, DataClassJsonMixin):
|
|
|
124
125
|
|
|
125
126
|
@dataclass(frozen=True)
|
|
126
127
|
class AnnotationCounterByInputData(AnnotationCounter, DataClassJsonMixin):
|
|
128
|
+
project_id: str
|
|
127
129
|
task_id: str
|
|
128
130
|
task_status: TaskStatus
|
|
129
131
|
task_phase: TaskPhase
|
|
@@ -254,6 +256,7 @@ class ListAnnotationCounterByInputData:
|
|
|
254
256
|
frame_no = self.frame_no_map.get((task_id, input_data_id))
|
|
255
257
|
|
|
256
258
|
return AnnotationCounterByInputData(
|
|
259
|
+
project_id=simple_annotation["project_id"],
|
|
257
260
|
task_id=simple_annotation["task_id"],
|
|
258
261
|
task_phase=TaskPhase(simple_annotation["task_phase"]),
|
|
259
262
|
task_phase_stage=simple_annotation["task_phase_stage"],
|
|
@@ -351,6 +354,7 @@ class ListAnnotationCounterByTask:
|
|
|
351
354
|
raise RuntimeError(f"{task_parser.task_id} ディレクトリにはjsonファイルが1つも含まれていません。")
|
|
352
355
|
|
|
353
356
|
return AnnotationCounterByTask(
|
|
357
|
+
project_id=last_simple_annotation["project_id"],
|
|
354
358
|
task_id=last_simple_annotation["task_id"],
|
|
355
359
|
task_status=TaskStatus(last_simple_annotation["task_status"]),
|
|
356
360
|
task_phase=TaskPhase(last_simple_annotation["task_phase"]),
|
|
@@ -468,6 +472,7 @@ class AttributeCountCsv:
|
|
|
468
472
|
) -> None:
|
|
469
473
|
def get_columns() -> list[AttributeValueKey]:
|
|
470
474
|
basic_columns = [
|
|
475
|
+
("project_id", "", ""),
|
|
471
476
|
("task_id", "", ""),
|
|
472
477
|
("task_status", "", ""),
|
|
473
478
|
("task_phase", "", ""),
|
|
@@ -480,6 +485,7 @@ class AttributeCountCsv:
|
|
|
480
485
|
|
|
481
486
|
def to_cell(c: AnnotationCounterByTask) -> dict[AttributeValueKey, Any]:
|
|
482
487
|
cell = {
|
|
488
|
+
("project_id", "", ""): c.project_id,
|
|
483
489
|
("task_id", "", ""): c.task_id,
|
|
484
490
|
("task_status", "", ""): c.task_status.value,
|
|
485
491
|
("task_phase", "", ""): c.task_phase.value,
|
|
@@ -506,6 +512,7 @@ class AttributeCountCsv:
|
|
|
506
512
|
) -> None:
|
|
507
513
|
def get_columns() -> list[AttributeValueKey]:
|
|
508
514
|
basic_columns = [
|
|
515
|
+
("project_id", "", ""),
|
|
509
516
|
("task_id", "", ""),
|
|
510
517
|
("task_status", "", ""),
|
|
511
518
|
("task_phase", "", ""),
|
|
@@ -520,6 +527,7 @@ class AttributeCountCsv:
|
|
|
520
527
|
|
|
521
528
|
def to_cell(c: AnnotationCounterByInputData) -> dict[tuple[str, str, str], Any]:
|
|
522
529
|
cell = {
|
|
530
|
+
("project_id", "", ""): c.project_id,
|
|
523
531
|
("input_data_id", "", ""): c.input_data_id,
|
|
524
532
|
("input_data_name", "", ""): c.input_data_name,
|
|
525
533
|
("frame_no", "", ""): c.frame_no,
|
|
@@ -569,6 +577,7 @@ class LabelCountCsv:
|
|
|
569
577
|
) -> None:
|
|
570
578
|
def get_columns() -> list[str]:
|
|
571
579
|
basic_columns = [
|
|
580
|
+
"project_id",
|
|
572
581
|
"task_id",
|
|
573
582
|
"task_status",
|
|
574
583
|
"task_phase",
|
|
@@ -581,6 +590,7 @@ class LabelCountCsv:
|
|
|
581
590
|
|
|
582
591
|
def to_dict(c: AnnotationCounterByTask) -> dict[str, Any]:
|
|
583
592
|
d = {
|
|
593
|
+
"project_id": c.project_id,
|
|
584
594
|
"task_id": c.task_id,
|
|
585
595
|
"task_status": c.task_status.value,
|
|
586
596
|
"task_phase": c.task_phase.value,
|
|
@@ -607,6 +617,7 @@ class LabelCountCsv:
|
|
|
607
617
|
) -> None:
|
|
608
618
|
def get_columns() -> list[str]:
|
|
609
619
|
basic_columns = [
|
|
620
|
+
"project_id",
|
|
610
621
|
"task_id",
|
|
611
622
|
"task_status",
|
|
612
623
|
"task_phase",
|
|
@@ -621,6 +632,7 @@ class LabelCountCsv:
|
|
|
621
632
|
|
|
622
633
|
def to_dict(c: AnnotationCounterByInputData) -> dict[str, Any]:
|
|
623
634
|
d = {
|
|
635
|
+
"project_id": c.project_id,
|
|
624
636
|
"input_data_id": c.input_data_id,
|
|
625
637
|
"input_data_name": c.input_data_name,
|
|
626
638
|
"frame_no": c.frame_no,
|
|
@@ -108,6 +108,7 @@ class AnnotationDuration(DataClassJsonMixin):
|
|
|
108
108
|
入力データまたはタスク単位の区間アノテーションの長さ情報。
|
|
109
109
|
"""
|
|
110
110
|
|
|
111
|
+
project_id: str
|
|
111
112
|
task_id: str
|
|
112
113
|
task_status: TaskStatus
|
|
113
114
|
task_phase: TaskPhase
|
|
@@ -234,6 +235,7 @@ class ListAnnotationDurationByInputData:
|
|
|
234
235
|
}
|
|
235
236
|
|
|
236
237
|
return AnnotationDuration(
|
|
238
|
+
project_id=simple_annotation["project_id"],
|
|
237
239
|
task_id=simple_annotation["task_id"],
|
|
238
240
|
task_phase=TaskPhase(simple_annotation["task_phase"]),
|
|
239
241
|
task_phase_stage=simple_annotation["task_phase_stage"],
|
|
@@ -360,6 +362,7 @@ class AnnotationDurationCsvByAttribute:
|
|
|
360
362
|
prior_attribute_columns: Optional[list[AttributeValueKey]] = None,
|
|
361
363
|
) -> list[AttributeValueKey]:
|
|
362
364
|
basic_columns = [
|
|
365
|
+
("project_id", "", ""),
|
|
363
366
|
("task_id", "", ""),
|
|
364
367
|
("task_status", "", ""),
|
|
365
368
|
("task_phase", "", ""),
|
|
@@ -379,6 +382,7 @@ class AnnotationDurationCsvByAttribute:
|
|
|
379
382
|
) -> pandas.DataFrame:
|
|
380
383
|
def to_cell(c: AnnotationDuration) -> dict[tuple[str, str, str], Any]:
|
|
381
384
|
cell: dict[AttributeValueKey, Any] = {
|
|
385
|
+
("project_id", "", ""): c.project_id,
|
|
382
386
|
("input_data_id", "", ""): c.input_data_id,
|
|
383
387
|
("input_data_name", "", ""): c.input_data_name,
|
|
384
388
|
("task_id", "", ""): c.task_id,
|
|
@@ -423,6 +427,7 @@ class AnnotationDurationCsvByLabel:
|
|
|
423
427
|
prior_label_columns: Optional[list[str]] = None,
|
|
424
428
|
) -> list[str]:
|
|
425
429
|
basic_columns = [
|
|
430
|
+
"project_id",
|
|
426
431
|
"task_id",
|
|
427
432
|
"task_status",
|
|
428
433
|
"task_phase",
|
|
@@ -442,6 +447,7 @@ class AnnotationDurationCsvByLabel:
|
|
|
442
447
|
) -> pandas.DataFrame:
|
|
443
448
|
def to_dict(c: AnnotationDuration) -> dict[str, Any]:
|
|
444
449
|
d: dict[str, Any] = {
|
|
450
|
+
"project_id": c.project_id,
|
|
445
451
|
"input_data_id": c.input_data_id,
|
|
446
452
|
"input_data_name": c.input_data_name,
|
|
447
453
|
"task_id": c.task_id,
|
|
@@ -38,7 +38,7 @@ def get_video_duration_list(task_list: list[dict[str, Any]], input_data_list: li
|
|
|
38
38
|
result = []
|
|
39
39
|
for task in task_list:
|
|
40
40
|
task_id = task["task_id"]
|
|
41
|
-
elm = {"task_id": task_id, "task_status": task["status"], "task_phase": task["phase"], "task_phase_stage": task["phase_stage"]}
|
|
41
|
+
elm = {"project_id": task["project_id"], "task_id": task_id, "task_status": task["status"], "task_phase": task["phase"], "task_phase_stage": task["phase_stage"]}
|
|
42
42
|
input_data_id_list = task["input_data_id_list"]
|
|
43
43
|
assert len(input_data_id_list) == 1, f"task_id='{task_id}'には複数の入力データが含まれています。"
|
|
44
44
|
input_data_id = input_data_id_list[0]
|
|
@@ -94,6 +94,7 @@ class ListVideoDuration(CommandLine):
|
|
|
94
94
|
logger.info(f"{len(video_duration_list)} 件のタスクの動画長さを出力します。")
|
|
95
95
|
if output_format == FormatArgument.CSV:
|
|
96
96
|
columns = [
|
|
97
|
+
"project_id",
|
|
97
98
|
"task_id",
|
|
98
99
|
"task_status",
|
|
99
100
|
"task_phase",
|
|
@@ -81,16 +81,34 @@ class ListTaskHistoryEventWithJson(CommandLine):
|
|
|
81
81
|
main_obj = ListTaskHistoryEventWithJsonMain(self.service)
|
|
82
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)
|
|
83
83
|
|
|
84
|
-
logger.debug(f"
|
|
85
|
-
|
|
86
|
-
if
|
|
87
|
-
|
|
84
|
+
logger.debug(f"{len(task_history_event_list)} 件のタスク履歴イベントの情報を出力します。")
|
|
85
|
+
|
|
86
|
+
if arg_format == FormatArgument.CSV:
|
|
87
|
+
columns = [
|
|
88
|
+
"project_id",
|
|
89
|
+
"task_id",
|
|
90
|
+
"task_history_id",
|
|
91
|
+
"created_datetime",
|
|
92
|
+
"phase",
|
|
93
|
+
"phase_stage",
|
|
94
|
+
"status",
|
|
95
|
+
"account_id",
|
|
96
|
+
"request.status",
|
|
97
|
+
"request.account_id",
|
|
98
|
+
"request.user_id",
|
|
99
|
+
"request.username",
|
|
100
|
+
"request.last_updated_datetime",
|
|
101
|
+
"request.force",
|
|
102
|
+
]
|
|
103
|
+
if len(task_history_event_list) > 0:
|
|
88
104
|
df = pandas.json_normalize(task_history_event_list)
|
|
89
|
-
|
|
105
|
+
df = df[columns]
|
|
90
106
|
else:
|
|
91
|
-
|
|
107
|
+
df = pandas.DataFrame(columns=columns)
|
|
108
|
+
|
|
109
|
+
self.print_csv(df)
|
|
92
110
|
else:
|
|
93
|
-
|
|
111
|
+
self.print_according_to_format(task_history_event_list)
|
|
94
112
|
|
|
95
113
|
def main(self) -> None:
|
|
96
114
|
args = self.args
|
|
@@ -36,6 +36,20 @@ class SimpleTaskHistoryEvent(DataClassJsonMixin):
|
|
|
36
36
|
status: str
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
@dataclass
|
|
40
|
+
class RequestOfTaskHistoryEvent(DataClassJsonMixin):
|
|
41
|
+
"""operateTask APIによってタスク履歴イベントが生成されたときのリクエストボディ
|
|
42
|
+
|
|
43
|
+
ただし、CLIユーザーにとって不要な情報は除いています。
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
status: str
|
|
47
|
+
force: bool
|
|
48
|
+
account_id: Optional[str]
|
|
49
|
+
user_id: Optional[str]
|
|
50
|
+
username: Optional[str]
|
|
51
|
+
|
|
52
|
+
|
|
39
53
|
@dataclass
|
|
40
54
|
class WorktimeFromTaskHistoryEvent(DataClassJsonMixin):
|
|
41
55
|
project_id: str
|
|
@@ -48,6 +62,8 @@ class WorktimeFromTaskHistoryEvent(DataClassJsonMixin):
|
|
|
48
62
|
worktime_hour: float
|
|
49
63
|
start_event: SimpleTaskHistoryEvent
|
|
50
64
|
end_event: SimpleTaskHistoryEvent
|
|
65
|
+
end_event_request: RequestOfTaskHistoryEvent
|
|
66
|
+
"""operateTask APIによってタスク履歴イベントが生成されたときのリクエストボディ"""
|
|
51
67
|
|
|
52
68
|
|
|
53
69
|
class ListWorktimeFromTaskHistoryEventMain:
|
|
@@ -123,6 +139,15 @@ class ListWorktimeFromTaskHistoryEventMain:
|
|
|
123
139
|
user_id = None
|
|
124
140
|
username = None
|
|
125
141
|
|
|
142
|
+
end_event_request_account_id = end_event["request"]["account_id"]
|
|
143
|
+
end_event_request_member = self.visualize.get_project_member_from_account_id(end_event_request_account_id)
|
|
144
|
+
if end_event_request_member is not None:
|
|
145
|
+
end_event_request_user_id = end_event_request_member["user_id"]
|
|
146
|
+
end_event_request_username = end_event_request_member["username"]
|
|
147
|
+
else:
|
|
148
|
+
end_event_request_user_id = None
|
|
149
|
+
end_event_request_username = None
|
|
150
|
+
|
|
126
151
|
return WorktimeFromTaskHistoryEvent(
|
|
127
152
|
# start_eventとend_eventの以下の属性は同じなので、start_eventの値を参照する
|
|
128
153
|
project_id=start_event["project_id"],
|
|
@@ -143,9 +168,16 @@ class ListWorktimeFromTaskHistoryEventMain:
|
|
|
143
168
|
created_datetime=end_event["created_datetime"],
|
|
144
169
|
status=end_event["status"],
|
|
145
170
|
),
|
|
171
|
+
end_event_request=RequestOfTaskHistoryEvent(
|
|
172
|
+
status=end_event["request"]["status"],
|
|
173
|
+
force=end_event["request"]["force"],
|
|
174
|
+
account_id=end_event["request"]["account_id"],
|
|
175
|
+
user_id=end_event_request_user_id,
|
|
176
|
+
username=end_event_request_username,
|
|
177
|
+
),
|
|
146
178
|
)
|
|
147
179
|
|
|
148
|
-
def _create_worktime_list(self, task_history_event_list: list[TaskHistoryEvent]) -> list[WorktimeFromTaskHistoryEvent]:
|
|
180
|
+
def _create_worktime_list(self, task_id: str, task_history_event_list: list[TaskHistoryEvent]) -> list[WorktimeFromTaskHistoryEvent]:
|
|
149
181
|
"""タスク履歴イベントから、作業時間のリストを生成する。
|
|
150
182
|
|
|
151
183
|
Args:
|
|
@@ -174,7 +206,12 @@ class ListWorktimeFromTaskHistoryEventMain:
|
|
|
174
206
|
TaskStatus.ON_HOLD.value,
|
|
175
207
|
TaskStatus.COMPLETE.value,
|
|
176
208
|
}:
|
|
177
|
-
logger.warning(
|
|
209
|
+
logger.warning(
|
|
210
|
+
f"task_id='{task_id}' :: 作業開始のイベント(task_history_id='{event['task_history_id']}')の次のイベント(task_history_id='{next_event['task_history_id']}')は、"
|
|
211
|
+
f"作業終了のイベントではないため、作業時間を算出できません。スキップします。"
|
|
212
|
+
f"タスク履歴イベントが不整合な状態なので、Annofabチームに問い合わせてください。 :: "
|
|
213
|
+
f"start_event='{start_event}', next_event='{next_event}'"
|
|
214
|
+
)
|
|
178
215
|
i += 1
|
|
179
216
|
continue
|
|
180
217
|
|
|
@@ -204,8 +241,8 @@ class ListWorktimeFromTaskHistoryEventMain:
|
|
|
204
241
|
task_history_event_dict = self._create_task_history_event_dict(all_task_history_event_list, task_ids=task_id_set, account_ids=account_id_set)
|
|
205
242
|
|
|
206
243
|
worktime_list = []
|
|
207
|
-
for subset_event_list in task_history_event_dict.
|
|
208
|
-
subset_worktime_list = self._create_worktime_list(subset_event_list)
|
|
244
|
+
for task_id, subset_event_list in task_history_event_dict.items():
|
|
245
|
+
subset_worktime_list = self._create_worktime_list(task_id, subset_event_list)
|
|
209
246
|
worktime_list.extend(subset_worktime_list)
|
|
210
247
|
return worktime_list
|
|
211
248
|
|
|
@@ -230,16 +267,38 @@ class ListWorktimeFromTaskHistoryEvent(CommandLine):
|
|
|
230
267
|
)
|
|
231
268
|
|
|
232
269
|
logger.debug(f"作業時間一覧の件数: {len(worktime_list)}")
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
270
|
+
dict_worktime_list = [e.to_dict() for e in worktime_list]
|
|
271
|
+
|
|
272
|
+
if arg_format == FormatArgument.CSV:
|
|
273
|
+
columns = [
|
|
274
|
+
"project_id",
|
|
275
|
+
"task_id",
|
|
276
|
+
"phase",
|
|
277
|
+
"phase_stage",
|
|
278
|
+
"account_id",
|
|
279
|
+
"user_id",
|
|
280
|
+
"username",
|
|
281
|
+
"worktime_hour",
|
|
282
|
+
"start_event.task_history_id",
|
|
283
|
+
"start_event.created_datetime",
|
|
284
|
+
"start_event.status",
|
|
285
|
+
"end_event.task_history_id",
|
|
286
|
+
"end_event.created_datetime",
|
|
287
|
+
"end_event.status",
|
|
288
|
+
"end_event_request.status",
|
|
289
|
+
"end_event_request.force",
|
|
290
|
+
"end_event_request.account_id",
|
|
291
|
+
"end_event_request.user_id",
|
|
292
|
+
"end_event_request.username",
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
if len(dict_worktime_list) > 0:
|
|
296
|
+
df = pandas.json_normalize(dict_worktime_list)[columns]
|
|
239
297
|
else:
|
|
240
|
-
|
|
298
|
+
df = pandas.DataFrame(columns=columns)
|
|
299
|
+
self.print_csv(df)
|
|
241
300
|
else:
|
|
242
|
-
|
|
301
|
+
self.print_according_to_format(dict_worktime_list)
|
|
243
302
|
|
|
244
303
|
def main(self) -> None:
|
|
245
304
|
args = self.args
|
|
@@ -2,20 +2,21 @@ annofabcli/__init__.py,sha256=fdBtxy5rOI8zi26jf0hmXS5KTBjQIsm2b9ZUSAIR558,319
|
|
|
2
2
|
annofabcli/__main__.py,sha256=83jLGTlNGoXaYU3fz9akYXoL3kgfIuaVXo_TkpK72IU,5267
|
|
3
3
|
annofabcli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
annofabcli/annotation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
annofabcli/annotation/annotation_query.py,sha256=
|
|
5
|
+
annofabcli/annotation/annotation_query.py,sha256=VwfPWpLOpVa2SeEJ264LmCKkBGDJvpX8o7GbWIrDE0o,15712
|
|
6
6
|
annofabcli/annotation/change_annotation_attributes.py,sha256=Zjqax-sb7ujJnTUVv4io0GFmZzfqx0rpN2N1W86uVTs,17092
|
|
7
|
+
annofabcli/annotation/change_annotation_attributes_per_annotation.py,sha256=7_the3-G18pu5YIlXQeyRhn9cUIFE3rlEPm-p9H8D50,12735
|
|
7
8
|
annofabcli/annotation/change_annotation_properties.py,sha256=Kp_LZ5sSoVmmjGE80ABVO3InxsXBIxiFFvVcIJNsOMk,18309
|
|
8
9
|
annofabcli/annotation/copy_annotation.py,sha256=Pih2k3vvpgfT3Ovb3gZw2L_8fK_ws_wKR7ARYG5hG_8,18407
|
|
9
|
-
annofabcli/annotation/delete_annotation.py,sha256=
|
|
10
|
+
annofabcli/annotation/delete_annotation.py,sha256=hQApNrx2Ci1bBWk0dRGA0oJkIgDHwl6Jy0-33gYF6jo,22989
|
|
10
11
|
annofabcli/annotation/download_annotation_zip.py,sha256=P_ZpdqIaSFEmB8jjpdykcRhh2tVlHxSlXFrYreJjShE,3282
|
|
11
|
-
annofabcli/annotation/dump_annotation.py,sha256=
|
|
12
|
+
annofabcli/annotation/dump_annotation.py,sha256=CJ10zJUm9C1M1VhB_ZtdkWPncrZTx9h9JyxmLn7f3lo,8397
|
|
12
13
|
annofabcli/annotation/import_annotation.py,sha256=39w-LwhNrT4-20NR5Hy-cn_cUFVVWVnMp9x1d8aITn0,33369
|
|
13
14
|
annofabcli/annotation/list_annotation.py,sha256=uKcOuGC7lzd6vVbzizkiZtYdXJ7EzY0iifuiqKl2wQM,10707
|
|
14
15
|
annofabcli/annotation/list_annotation_count.py,sha256=T9fbaoxWeDJIVgW_YgHRldbwrVZWiE-57lfJrDQrj80,6474
|
|
15
16
|
annofabcli/annotation/merge_segmentation.py,sha256=kIsCeXtJxzd6nobQPpi0fscaRDlTx3tg1qpy5PDfSJI,18107
|
|
16
17
|
annofabcli/annotation/remove_segmentation_overlap.py,sha256=JeeBY3PUcBqLYiNItEH8Ae9zhEy6cf4LldNr_4yBdjY,15991
|
|
17
|
-
annofabcli/annotation/restore_annotation.py,sha256=
|
|
18
|
-
annofabcli/annotation/subcommand_annotation.py,sha256=
|
|
18
|
+
annofabcli/annotation/restore_annotation.py,sha256=F0_p1k0Yzx0YVOuLYOS48OWutsN5ysOu3TRa3yhRdTw,17071
|
|
19
|
+
annofabcli/annotation/subcommand_annotation.py,sha256=PfiU4G_Col3quCXKbZpqWoUXGi5p2uB1XI9BKMwaq1I,2288
|
|
19
20
|
annofabcli/annotation_specs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
21
|
annofabcli/annotation_specs/add_attribute_restriction.py,sha256=jVA7ltharabR94bf71QuW0x0yVgu6-A7PHn7Dq3kUfg,6825
|
|
21
22
|
annofabcli/annotation_specs/attribute_restriction.py,sha256=tXcCLWVUdmmLWFHLjtSERcgxwFhqk7_zyKZzps0oyso,10123
|
|
@@ -37,7 +38,7 @@ annofabcli/comment/delete_comment.py,sha256=NpQVrXrIPI3vjAyOZ-5Mm-Pae_uCHu8BXVEA
|
|
|
37
38
|
annofabcli/comment/download_comment_json.py,sha256=YfqUnMgvLgVFV7FJPIXwirREkQ2E63fXeCaF4hfwk8c,2338
|
|
38
39
|
annofabcli/comment/list_all_comment.py,sha256=zMZSmVe8P89WDBZE5PZn5SIv5j2OTbnP1kS-HnCxPCI,6483
|
|
39
40
|
annofabcli/comment/list_comment.py,sha256=ikXOtfI9W88w7YC1GQQ1ryq2BatHelIRb8Pa5-fKbSs,6187
|
|
40
|
-
annofabcli/comment/put_comment.py,sha256=
|
|
41
|
+
annofabcli/comment/put_comment.py,sha256=7q_GQW7nKUqzm9tf7mjhiPHfd0lBFLFvvdyF5Cn9854,12025
|
|
41
42
|
annofabcli/comment/put_comment_simply.py,sha256=OwanjmQy5nqqIw-i0Gt9APdEdftM-EuZW4pRLYbIwQM,8171
|
|
42
43
|
annofabcli/comment/put_inspection_comment.py,sha256=TL8o_K9LCLxqMEVPs9wNK_X4jzEiJwjr327lziQbeAM,3803
|
|
43
44
|
annofabcli/comment/put_inspection_comment_simply.py,sha256=WuySt-BV4oPETBIgLwpCIH-hh8XRetIHPb6Gtnpgs7A,6805
|
|
@@ -58,7 +59,7 @@ annofabcli/common/pandas.py,sha256=IW9xqHkdRF1I6YZc7CP_9tkGxJuu1MKEXFILjhaNUU0,4
|
|
|
58
59
|
annofabcli/common/type_util.py,sha256=i3r5pFtRYQwJrYkl1-lVQi8XOePQxTUX_cAHgBTsagM,224
|
|
59
60
|
annofabcli/common/typing.py,sha256=_AcEogoaMbib0esfN2RvHshAZH0oyRb2Ro9-rbn7NJ8,330
|
|
60
61
|
annofabcli/common/utils.py,sha256=Eb4DS5j_EVdZW_YnsvIgjTbgWcjHd560plck2_WgrCI,9545
|
|
61
|
-
annofabcli/common/visualize.py,sha256=
|
|
62
|
+
annofabcli/common/visualize.py,sha256=mBPCA5vfYNbE3I1m5oHRpo-E1hsNH11KswILtYRCOKQ,13650
|
|
62
63
|
annofabcli/common/annofab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
63
64
|
annofabcli/common/annofab/annotation_specs.py,sha256=h-YSnuK0riCLoJMmV-KDP3JS-_JMqx0feSvPAB3xCSI,765
|
|
64
65
|
annofabcli/common/annofab/project.py,sha256=BK96o9OO961yfUvcjuHG1DeJ2bMMEYTS5ujtJCvD_4I,891
|
|
@@ -136,12 +137,12 @@ annofabcli/stat_visualization/write_performance_rating_csv.py,sha256=0j78z5uRULo
|
|
|
136
137
|
annofabcli/statistics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
137
138
|
annofabcli/statistics/histogram.py,sha256=CvzDxT2cKLSnBGSqkZE6p92PayGxYYja1YyB24M4ALU,3245
|
|
138
139
|
annofabcli/statistics/linegraph.py,sha256=0kr7jVBNMiM2ECYhv3Ry5RitElKerSl9ZKxbKzfiplI,12494
|
|
139
|
-
annofabcli/statistics/list_annotation_area.py,sha256=
|
|
140
|
-
annofabcli/statistics/list_annotation_attribute.py,sha256=
|
|
141
|
-
annofabcli/statistics/list_annotation_attribute_filled_count.py,sha256=
|
|
142
|
-
annofabcli/statistics/list_annotation_count.py,sha256=
|
|
143
|
-
annofabcli/statistics/list_annotation_duration.py,sha256=
|
|
144
|
-
annofabcli/statistics/list_video_duration.py,sha256=
|
|
140
|
+
annofabcli/statistics/list_annotation_area.py,sha256=nGqhqPEHLPUccnLCmA9nmwvmYbZYlh5ZxiHNszZ1ei8,12329
|
|
141
|
+
annofabcli/statistics/list_annotation_attribute.py,sha256=L_wmcUGS0Lu3tnRmidDjYK3bCNdvwDjAH64r5__MQ_w,12522
|
|
142
|
+
annofabcli/statistics/list_annotation_attribute_filled_count.py,sha256=g-MihJwEoA1ZnLTJOGX-dn6hI5aUdqQvh2KWbREbQ9s,29033
|
|
143
|
+
annofabcli/statistics/list_annotation_count.py,sha256=cBmUTYifB7UESEFets7DMDn8_GKDBBDJ2vt1q_56fcc,52747
|
|
144
|
+
annofabcli/statistics/list_annotation_duration.py,sha256=ps0UT50W_6ZL5_cZNsSo7Dp-cSx77m1IF4QMBkwRt98,31875
|
|
145
|
+
annofabcli/statistics/list_video_duration.py,sha256=OCDXPOP2z7flE8NBaQD1lDkU9JG5mn3PfACijoR-5s0,9163
|
|
145
146
|
annofabcli/statistics/list_worktime.py,sha256=nr--GtFY-oyFuu8M0EsUqcVxX26gjeP09LYUcdeptyk,6456
|
|
146
147
|
annofabcli/statistics/scatter.py,sha256=C3hTlm_QfGBiY4KjZ-8D_u_Rk53a9f4jszx4iNZgp9w,10945
|
|
147
148
|
annofabcli/statistics/subcommand_statistics.py,sha256=Pvd7s0vvDU9tSpAphPrv94IDhhR1p8iFH2tjdt7I7ZU,2536
|
|
@@ -205,11 +206,11 @@ annofabcli/task_history/list_task_history.py,sha256=izzY9PzgrgJW0O3ftAoSEfqa9fK5
|
|
|
205
206
|
annofabcli/task_history/subcommand_task_history.py,sha256=_CvM1ts60GT15iXmsb4CHOyL170ier-q-p9edVuNLvc,1066
|
|
206
207
|
annofabcli/task_history_event/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
207
208
|
annofabcli/task_history_event/download_task_history_event_json.py,sha256=hQLVbQ0HjdiRpXC9tEt_Py3W_b0mo3eVIa-zp0z3fgU,2434
|
|
208
|
-
annofabcli/task_history_event/list_all_task_history_event.py,sha256=
|
|
209
|
-
annofabcli/task_history_event/list_worktime.py,sha256=
|
|
209
|
+
annofabcli/task_history_event/list_all_task_history_event.py,sha256=EeKMyPUxGwYCFtWQHHW954ZserGm8lUqrwNnV1iX9X4,6830
|
|
210
|
+
annofabcli/task_history_event/list_worktime.py,sha256=Y7Pu5DP7scPf7HPt6CTiTvB1_5_Nfi1bStUIaCpkhII,15507
|
|
210
211
|
annofabcli/task_history_event/subcommand_task_history_event.py,sha256=mJVJoT4RXk4HWnY7-Nrsl4If-gtaIIEXd2z7eFZwM2I,1260
|
|
211
|
-
annofabcli-1.
|
|
212
|
-
annofabcli-1.
|
|
213
|
-
annofabcli-1.
|
|
214
|
-
annofabcli-1.
|
|
215
|
-
annofabcli-1.
|
|
212
|
+
annofabcli-1.105.0.dist-info/METADATA,sha256=HJ0IsIuktrGOeyum7nYk72MiPAhXOw03g-E2wiB0Hv4,5286
|
|
213
|
+
annofabcli-1.105.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
214
|
+
annofabcli-1.105.0.dist-info/entry_points.txt,sha256=C2uSUc-kkLJpoK_mDL5FEMAdorLEMPfwSf8VBMYnIFM,56
|
|
215
|
+
annofabcli-1.105.0.dist-info/licenses/LICENSE,sha256=pcqWYfxFtxBzhvKp3x9MXNM4xciGb2eFewaRhXUNHlo,1081
|
|
216
|
+
annofabcli-1.105.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|