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.
@@ -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
- # TODO どこかのタイミングで、"v=2"のアノテーションを取得するようにする
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
- input_data_id = editor_annotation["input_data_id"]
39
- outer_dir = json_path.parent / input_data_id
40
- outer_dir.mkdir(exist_ok=True, parents=True)
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
- for detail in outer_details:
44
- annotation_id = detail["annotation_id"]
45
- outer_file_path = outer_dir / f"{annotation_id}"
46
- self.service.wrapper.download(detail["url"], outer_file_path)
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 parser_to_request_body(self, parser: SimpleAnnotationParser) -> dict[str, Any]:
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(parser.load_json(), infer_missing=True)
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
 
@@ -375,13 +375,10 @@ class AddProps:
375
375
 
376
376
  * user_id
377
377
  * username
378
-
379
- Args:
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"タスク履歴イベント一覧の件数: {len(task_history_event_list)}")
85
-
86
- if len(task_history_event_list) > 0:
87
- if arg_format == FormatArgument.CSV:
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
- self.print_csv(df)
105
+ df = df[columns]
90
106
  else:
91
- self.print_according_to_format(task_history_event_list)
107
+ df = pandas.DataFrame(columns=columns)
108
+
109
+ self.print_csv(df)
92
110
  else:
93
- logger.warning("タスク履歴イベント一覧の件数が0件であるため、出力しません。")
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(f"作業中状態のタスク履歴イベントに対応するタスク履歴イベントが存在しませんでした。:: start_event={start_event}, next_event={next_event}")
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.values():
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
- if len(worktime_list) > 0:
235
- dict_worktime_list = [e.to_dict() for e in worktime_list]
236
- if arg_format == FormatArgument.CSV:
237
- df = pandas.json_normalize(dict_worktime_list)
238
- self.print_csv(df)
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
- self.print_according_to_format(dict_worktime_list)
298
+ df = pandas.DataFrame(columns=columns)
299
+ self.print_csv(df)
241
300
  else:
242
- logger.warning("作業時間一覧の件数が0件であるため、出力しません。")
301
+ self.print_according_to_format(dict_worktime_list)
243
302
 
244
303
  def main(self) -> None:
245
304
  args = self.args
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: annofabcli
3
- Version: 1.104.0
3
+ Version: 1.105.0
4
4
  Summary: Utility Command Line Interface for AnnoFab
5
5
  Author: Kurusugawa Computer Inc.
6
6
  License: MIT
@@ -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=loXcmHhwp0gh_4u5NhvA1x2VAbIT6UJpdIzeFHT5HlY,15566
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=Yo73sl96ETMnf6uscJX4h3NU3-cT-OH-rqqXMoLlhbM,23105
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=iiTYjbUYEJrgoSfr4oQOepVkba9O4sjvpSKYfuER318,7466
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=_XM1BHIZL-cxwEO6zlqy_wKE3sSS9NJGpU-WzIR2c9k,14616
18
- annofabcli/annotation/subcommand_annotation.py,sha256=ku9mzb7zZilHcjf1MFV1E7EJ8OvfSUDHpcunM38teto,2122
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=aP1VhjwzKLY8zjHOO1ZkHc6ulwpKtAt-10fWpDn3RUo,12000
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=94lPfp6jc9zhCntScuzTv_UvYiokb-l_sCwB9pjW9tA,13612
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=K1V1w7eNXv-6P_7v3SXqO6VQx8FF6Yjl7OL0IeWiDO4,12227
140
- annofabcli/statistics/list_annotation_attribute.py,sha256=aq3wU2edygRygd3uydhh7dPBUQlyX5zQK3H4b3RiARI,12420
141
- annofabcli/statistics/list_annotation_attribute_filled_count.py,sha256=ryDLtFIdW2G5YDqYZWU2-p7L8oGAXSE6_OknSVoZLy0,28708
142
- annofabcli/statistics/list_annotation_count.py,sha256=sYepVd6UDqy2O3jUO4f6QqSRCUzTi_HorydKJcL4g5Q,52254
143
- annofabcli/statistics/list_annotation_duration.py,sha256=ZW_sSQQdUAyv14wAIKghGzUe_g1vxusWK_P2e9oGa8s,31639
144
- annofabcli/statistics/list_video_duration.py,sha256=WGM4Onucw4Z8az8how5FvpN5u-YQaxAEnIJblKY7gl8,9099
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=mzFqOS1WjC4_QEz6Yyp1ZvU5h-kU_PU82CtnnjQRSlY,6345
209
- annofabcli/task_history_event/list_worktime.py,sha256=J18uLrQwZuaatkA4dAr83Rz_qAF2xsx2gOFDp6ysimI,12968
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.104.0.dist-info/METADATA,sha256=QfZgIqtN2dKAfkARjsETvHWWcbG63dUGdi1p2yDAjB4,5286
212
- annofabcli-1.104.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
213
- annofabcli-1.104.0.dist-info/entry_points.txt,sha256=C2uSUc-kkLJpoK_mDL5FEMAdorLEMPfwSf8VBMYnIFM,56
214
- annofabcli-1.104.0.dist-info/licenses/LICENSE,sha256=pcqWYfxFtxBzhvKp3x9MXNM4xciGb2eFewaRhXUNHlo,1081
215
- annofabcli-1.104.0.dist-info/RECORD,,
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,,