annofabcli 1.100.5__py3-none-any.whl → 1.102.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.
Files changed (30) hide show
  1. annofabcli/annotation/change_annotation_attributes.py +3 -2
  2. annofabcli/annotation/change_annotation_properties.py +8 -7
  3. annofabcli/annotation/copy_annotation.py +4 -4
  4. annofabcli/annotation/delete_annotation.py +254 -29
  5. annofabcli/annotation/dump_annotation.py +14 -7
  6. annofabcli/annotation/import_annotation.py +304 -227
  7. annofabcli/annotation/restore_annotation.py +7 -7
  8. annofabcli/annotation_specs/list_annotation_specs_attribute.py +1 -1
  9. annofabcli/annotation_specs/list_annotation_specs_choice.py +1 -1
  10. annofabcli/annotation_specs/list_annotation_specs_label.py +1 -1
  11. annofabcli/annotation_specs/list_annotation_specs_label_attribute.py +1 -1
  12. annofabcli/comment/delete_comment.py +7 -5
  13. annofabcli/comment/put_comment.py +1 -1
  14. annofabcli/comment/put_comment_simply.py +7 -5
  15. annofabcli/common/cli.py +10 -10
  16. annofabcli/common/download.py +28 -29
  17. annofabcli/common/image.py +4 -2
  18. annofabcli/input_data/delete_input_data.py +4 -4
  19. annofabcli/input_data/update_metadata_of_input_data.py +1 -1
  20. annofabcli/instruction/upload_instruction.py +2 -2
  21. annofabcli/statistics/list_annotation_area.py +320 -0
  22. annofabcli/statistics/subcommand_statistics.py +2 -0
  23. annofabcli/statistics/visualize_statistics.py +1 -7
  24. annofabcli/supplementary/delete_supplementary_data.py +8 -4
  25. {annofabcli-1.100.5.dist-info → annofabcli-1.102.0.dist-info}/METADATA +3 -2
  26. {annofabcli-1.100.5.dist-info → annofabcli-1.102.0.dist-info}/RECORD +29 -29
  27. annofabcli/__version__.py +0 -1
  28. {annofabcli-1.100.5.dist-info → annofabcli-1.102.0.dist-info}/WHEEL +0 -0
  29. {annofabcli-1.100.5.dist-info → annofabcli-1.102.0.dist-info}/entry_points.txt +0 -0
  30. {annofabcli-1.100.5.dist-info → annofabcli-1.102.0.dist-info}/licenses/LICENSE +0 -0
@@ -13,13 +13,10 @@ from pathlib import Path
13
13
  from typing import Any, Optional, Union
14
14
 
15
15
  import annofabapi
16
- from annofabapi.dataclass.annotation import AdditionalDataV1, AnnotationDetailV1
16
+ import ulid
17
17
  from annofabapi.models import (
18
18
  AdditionalDataDefinitionType,
19
- AdditionalDataDefinitionV1,
20
- AnnotationDataHoldingType,
21
19
  DefaultAnnotationType,
22
- LabelV1,
23
20
  ProjectMemberRole,
24
21
  TaskStatus,
25
22
  )
@@ -29,10 +26,11 @@ from annofabapi.parser import (
29
26
  lazy_parse_simple_annotation_dir_by_task,
30
27
  lazy_parse_simple_annotation_zip_by_task,
31
28
  )
32
- from annofabapi.plugin import ThreeDimensionAnnotationType
33
- from annofabapi.utils import can_put_annotation, str_now
29
+ from annofabapi.plugin import EditorPluginId, ThreeDimensionAnnotationType
30
+ from annofabapi.pydantic_models.input_data_type import InputDataType
31
+ from annofabapi.util.annotation_specs import AnnotationSpecsAccessor, get_choice
32
+ from annofabapi.utils import can_put_annotation
34
33
  from dataclasses_json import DataClassJsonMixin
35
- from more_itertools import first_true
36
34
 
37
35
  import annofabcli
38
36
  from annofabcli.common.cli import (
@@ -44,7 +42,8 @@ from annofabcli.common.cli import (
44
42
  build_annofabapi_resource_and_login,
45
43
  )
46
44
  from annofabcli.common.facade import AnnofabApiFacade
47
- from annofabcli.common.visualize import AddProps, MessageLocale
45
+ from annofabcli.common.type_util import assert_noreturn
46
+ from annofabcli.common.visualize import AddProps
48
47
 
49
48
  logger = logging.getLogger(__name__)
50
49
 
@@ -78,272 +77,310 @@ class ImportedSimpleAnnotation(DataClassJsonMixin):
78
77
  """矩形、ポリゴン、全体アノテーションなど個々のアノテーションの配列。"""
79
78
 
80
79
 
81
- class ImportAnnotationMain(CommandLineWithConfirm):
82
- def __init__(
83
- self,
84
- service: annofabapi.Resource,
85
- *,
86
- project_id: str,
87
- all_yes: bool,
88
- is_force: bool,
89
- is_merge: bool,
90
- is_overwrite: bool,
91
- ) -> None:
92
- self.service = service
93
- self.facade = AnnofabApiFacade(service)
94
- CommandLineWithConfirm.__init__(self, all_yes)
80
+ def is_image_segmentation_label(label_info: dict[str, Any]) -> bool:
81
+ """
82
+ ラベルの種類が、画像プロジェクトのセグメンテーションかどうか
83
+ """
84
+ annotation_type = label_info["annotation_type"]
85
+ return annotation_type in {DefaultAnnotationType.SEGMENTATION.value, DefaultAnnotationType.SEGMENTATION_V2.value}
95
86
 
96
- self.project_id = project_id
97
- self.is_force = is_force
98
- self.is_merge = is_merge
99
- self.is_overwrite = is_overwrite
100
- self.visualize = AddProps(service, project_id)
101
87
 
102
- def get_label_info_from_label_name(self, label_name: str) -> Optional[LabelV1]:
103
- for label in self.visualize.specs_labels:
104
- label_name_en = self.visualize.get_label_name(label["label_id"], MessageLocale.EN)
105
- if label_name_en is not None and label_name_en == label_name:
106
- return label
88
+ def is_3dpc_segment_label(label_info: dict[str, Any]) -> bool:
89
+ """
90
+ 3次元のセグメントかどうか
91
+ """
92
+ # 理想はプラグイン情報を見て、3次元アノテーションの種類かどうか判定した方がよい
93
+ # が、当分は文字列判定だけでも十分なので、文字列で判定する
94
+ return label_info["annotation_type"] in {
95
+ ThreeDimensionAnnotationType.INSTANCE_SEGMENT.value,
96
+ ThreeDimensionAnnotationType.SEMANTIC_SEGMENT.value,
97
+ }
107
98
 
108
- logger.warning(f"アノテーション仕様に label_name='{label_name}' のラベルが存在しません。")
109
- return None
110
99
 
111
- def _get_additional_data_from_attribute_name(self, attribute_name: str, label_info: LabelV1) -> Optional[AdditionalDataDefinitionV1]:
112
- for additional_data in label_info["additional_data_definitions"]:
113
- additional_data_name_en = self.visualize.get_additional_data_name(
114
- additional_data["additional_data_definition_id"], MessageLocale.EN, label_id=label_info["label_id"]
115
- )
116
- if additional_data_name_en is not None and additional_data_name_en == attribute_name:
117
- return additional_data
100
+ def is_3dpc_project(project: dict[str, Any]) -> bool:
101
+ """3次元プロジェクトか否か"""
102
+ editor_plugin_id = project["configuration"]["plugin_id"]
103
+ return project["input_data_type"] == InputDataType.CUSTOM.value and editor_plugin_id == EditorPluginId.THREE_DIMENSION.value
104
+
118
105
 
119
- return None
106
+ def create_annotation_id(label_info: dict[str, Any], project: dict[str, Any]) -> str:
107
+ """
108
+ デフォルトのアノテーションIDを生成します。
109
+ """
110
+ if is_3dpc_project(project):
111
+ # 3次元エディタ画面ではULIDが発行されるので、それに合わせる
112
+ return str(ulid.new())
113
+ elif label_info["annotation_type"] == DefaultAnnotationType.CLASSIFICATION.value:
114
+ # 全体アノテーションの場合、annotation_idはlabel_idである必要がある
115
+ return label_info["label_id"]
116
+ else:
117
+ return str(uuid.uuid4())
118
+
119
+
120
+ def get_3dpc_segment_data_uri(annotation_data: dict[str, Any]) -> str:
121
+ """
122
+ 3DセグメントのURIを取得する
123
+
124
+ Notes:
125
+ 3dpc editorに依存したコード。annofab側でSimple Annotationのフォーマットが改善されたら、このコードを削除する
126
+ """
127
+ data_uri = annotation_data["data"]
128
+ # この時点で data_uriは f"./{input_data_id}/{annotation_id}"
129
+ # parser.open_data_uriメソッドに渡す値は、先頭のinput_data_idは不要なので、これを取り除く
130
+ path = Path(data_uri)
131
+ return str(path.relative_to(path.parts[0]))
132
+
133
+
134
+ class AnnotationConverter:
135
+ """
136
+ Simpleアノテーションを、`put_annotation` API(v2)のリクエストボディに変換するクラスです。
137
+
138
+ Args:
139
+ project: プロジェクト情報
140
+ annotation_specs: アノテーション仕様情報(v3)
141
+ is_strict: Trueの場合、存在しない属性名や属性値の型不一致があった場合に例外を発生させる
142
+ service: Annofab APIにアクセスするためのサービスオブジェクト。外部アノテーションをアップロードするのに利用する。
143
+ """
144
+
145
+ def __init__(self, project: dict[str, Any], annotation_specs: dict[str, Any], *, service: annofabapi.Resource, is_strict: bool = False) -> None:
146
+ self.project = project
147
+ self.project_id = project["project_id"]
148
+ self.annotation_specs = annotation_specs
149
+ self.annotation_specs_accessor = AnnotationSpecsAccessor(annotation_specs)
150
+ self.is_strict = is_strict
151
+ self.service = service
152
+
153
+ def _convert_attribute_value( # noqa: PLR0911, PLR0912
154
+ self,
155
+ attribute_value: Union[str, int, bool], # noqa: FBT001
156
+ additional_data_type: AdditionalDataDefinitionType,
157
+ attribute_name: str,
158
+ choices: list[dict[str, Any]],
159
+ *,
160
+ log_message_suffix: str,
161
+ ) -> Optional[dict[str, Any]]:
162
+ if additional_data_type == AdditionalDataDefinitionType.FLAG:
163
+ if not isinstance(attribute_value, bool):
164
+ message = f"属性'{attribute_name}'に対応する属性値の型は bool である必要があります。 :: attribute_value='{attribute_value}', additional_data_type='{additional_data_type}' :: {log_message_suffix}" # noqa: E501
165
+ logger.warning(message)
166
+ if self.is_strict:
167
+ raise ValueError(message)
168
+ return None
169
+ return {"_type": "Flag", "value": attribute_value}
170
+
171
+ elif additional_data_type == AdditionalDataDefinitionType.INTEGER:
172
+ if not isinstance(attribute_value, int):
173
+ message = f"属性'{attribute_name}'に対応する属性値の型は int である必要があります。 :: attribute_value='{attribute_value}', additional_data_type='{additional_data_type}' :: {log_message_suffix}" # noqa: E501
174
+ logger.warning(message)
175
+ if self.is_strict:
176
+ raise ValueError(message)
177
+ return None
178
+ return {"_type": "Integer", "value": attribute_value}
179
+
180
+ # 以降の属性は、属性値の型はstr型であるが、型をチェックしない。
181
+ # str型に変換しても、特に期待していない動作にならないと思われるため
182
+ elif additional_data_type == AdditionalDataDefinitionType.COMMENT:
183
+ return {"_type": "Comment", "value": str(attribute_value)}
184
+
185
+ elif additional_data_type == AdditionalDataDefinitionType.TEXT:
186
+ return {"_type": "Text", "value": str(attribute_value)}
187
+
188
+ elif additional_data_type == AdditionalDataDefinitionType.TRACKING:
189
+ return {"_type": "Tracking", "value": str(attribute_value)}
190
+
191
+ elif additional_data_type == AdditionalDataDefinitionType.LINK:
192
+ if attribute_value == "":
193
+ # `annotation_id`に空文字を設定すると、エディタ画面ではエラーになるため、Noneを返す
194
+ return None
195
+ return {"_type": "Link", "annotation_id": str(attribute_value)}
196
+
197
+ elif additional_data_type == AdditionalDataDefinitionType.CHOICE:
198
+ if attribute_value == "":
199
+ return None
200
+ try:
201
+ choice = get_choice(choices, choice_name=str(attribute_value))
202
+ except ValueError:
203
+ logger.warning(
204
+ f"アノテーション仕様の属性'{attribute_name}'に選択肢名(英語)が'{attribute_value}'である選択肢情報は存在しないか、複数存在します。 :: {log_message_suffix}" # noqa: E501
205
+ )
206
+ if self.is_strict:
207
+ raise
208
+ return None
209
+
210
+ return {"_type": "Choice", "choice_id": choice["choice_id"]}
211
+ elif additional_data_type == AdditionalDataDefinitionType.SELECT:
212
+ if attribute_value == "":
213
+ return None
214
+ try:
215
+ choice = get_choice(choices, choice_name=str(attribute_value))
216
+ except ValueError:
217
+ logger.warning(
218
+ f"アノテーション仕様の属性'{attribute_name}'に選択肢名(英語)が'{attribute_value}'である選択肢情報は存在しないか、複数存在します。 :: {log_message_suffix}" # noqa: E501
219
+ )
220
+ if self.is_strict:
221
+ raise
222
+ return None
223
+
224
+ return {"_type": "Select", "choice_id": choice["choice_id"]}
120
225
 
121
- def _get_choice_id_from_name(self, name: str, choices: list[dict[str, Any]]) -> Optional[str]:
122
- choice_info = first_true(choices, pred=lambda e: self.facade.get_choice_name_en(e) == name)
123
- if choice_info is not None:
124
- return choice_info["choice_id"]
125
226
  else:
126
- return None
227
+ assert_noreturn(additional_data_type)
127
228
 
128
- @classmethod
129
- def _get_3dpc_segment_data_uri(cls, annotation_data: dict[str, Any]) -> str:
229
+ def convert_attributes(
230
+ self, attributes: dict[str, Any], *, label_name: Optional[str] = None, log_message_suffix: str = ""
231
+ ) -> list[dict[str, Any]]:
130
232
  """
131
- 3DセグメントのURIを取得する
233
+ インポート対象のアノテーションJSONに格納されている`attributes`を`AdditionalDataListV2`のlistに変換します。
132
234
 
133
- Notes:
134
- 3dpc editorに依存したコード。annofab側でSimple Annotationのフォーマットが改善されたら、このコードを削除する
135
- """
136
- data_uri = annotation_data["data"]
137
- # この時点で data_uriは f"./{input_data_id}/{annotation_id}"
138
- # parser.open_data_uriメソッドに渡す値は、先頭のinput_data_idは不要なので、これを取り除く
139
- path = Path(data_uri)
140
- return str(path.relative_to(path.parts[0]))
141
-
142
- @classmethod
143
- def _is_3dpc_segment_label(cls, label_info: dict[str, Any]) -> bool:
144
- """
145
- 3次元のセグメントかどうか
235
+ Args:
236
+ attributes: インポート対象のアノテーションJSONに格納されている`attributes`
237
+ label_name: `attributes`に紐づくラベルの名前。指定した場合は、指定したラベルに紐づく属性を探します。
238
+ log_message_suffix: ログメッセージのサフィックス
239
+
240
+ Raises:
241
+ ValueError: `self.is_strict`がTrueの場合:存在しない属性名が指定されたり、属性値の型が適切でない
146
242
  """
147
- # 理想はプラグイン情報を見て、3次元アノテーションの種類かどうか判定した方がよい
148
- # が、当分は文字列判定だけでも十分なので、文字列で判定する
149
- if label_info["annotation_type"] in {
150
- ThreeDimensionAnnotationType.INSTANCE_SEGMENT.value,
151
- ThreeDimensionAnnotationType.SEMANTIC_SEGMENT.value,
152
- }:
153
- return True
154
-
155
- # 標準の3次元アノテーション仕様用のプラグインを利用していない場合
156
- # TODO すべての3次元プロジェクトが、標準の3次元アノテーション仕様用のプラグインを利用するようになったら、この処理を削除する
157
- if label_info["annotation_type"] == DefaultAnnotationType.CUSTOM.value:
158
- metadata = label_info["metadata"]
159
- if metadata.get("type") == "SEGMENT":
160
- return True
161
-
162
- return False
163
-
164
- @classmethod
165
- def _get_data_holding_type_from_data(cls, label_info: dict[str, Any]) -> AnnotationDataHoldingType:
166
- annotation_type = label_info["annotation_type"]
167
- if annotation_type in [DefaultAnnotationType.SEGMENTATION.value, DefaultAnnotationType.SEGMENTATION_V2.value]:
168
- return AnnotationDataHoldingType.OUTER
169
-
170
- # TODO: 3dpc editorに依存したコード。annofab側でSimple Annotationのフォーマットが改善されたら、このコードを削除する
171
- if cls._is_3dpc_segment_label(label_info):
172
- return AnnotationDataHoldingType.OUTER
173
-
174
- return AnnotationDataHoldingType.INNER
175
-
176
- def _to_additional_data_list(self, attributes: dict[str, Any], label_info: LabelV1) -> list[AdditionalDataV1]:
177
- additional_data_list: list[AdditionalDataV1] = []
178
- for key, value in attributes.items():
179
- specs_additional_data = self._get_additional_data_from_attribute_name(key, label_info)
180
- if specs_additional_data is None:
181
- logger.warning(f"アノテーション仕様に attribute_name='{key}' が存在しません。")
182
- continue
243
+ additional_data_list: list[dict[str, Any]] = []
183
244
 
184
- additional_data = AdditionalDataV1(
185
- additional_data_definition_id=specs_additional_data["additional_data_definition_id"],
186
- flag=None,
187
- integer=None,
188
- choice=None,
189
- comment=None,
190
- )
191
- additional_data_type = AdditionalDataDefinitionType(specs_additional_data["type"])
192
- if additional_data_type == AdditionalDataDefinitionType.FLAG:
193
- additional_data.flag = value
194
- elif additional_data_type == AdditionalDataDefinitionType.INTEGER:
195
- additional_data.integer = value
196
- elif additional_data_type in [
197
- AdditionalDataDefinitionType.TEXT,
198
- AdditionalDataDefinitionType.COMMENT,
199
- AdditionalDataDefinitionType.TRACKING,
200
- AdditionalDataDefinitionType.LINK,
201
- ]:
202
- additional_data.comment = value
203
- elif additional_data_type in [AdditionalDataDefinitionType.CHOICE, AdditionalDataDefinitionType.SELECT]:
204
- additional_data.choice = self._get_choice_id_from_name(value, specs_additional_data["choices"])
205
- else:
206
- logger.warning(f"additional_data_type='{additional_data_type}'が不正です。")
245
+ label = self.annotation_specs_accessor.get_label(label_name=label_name) if label_name is not None else None
246
+
247
+ for attribute_name, attribute_value in attributes.items():
248
+ try:
249
+ specs_additional_data = self.annotation_specs_accessor.get_attribute(attribute_name=attribute_name, label=label)
250
+ except ValueError:
251
+ logger.warning(
252
+ f"アノテーション仕様に属性名(英語)が'{attribute_name}'である属性情報が存在しないか、複数存在します。 :: {log_message_suffix}"
253
+ )
254
+ if self.is_strict:
255
+ raise
207
256
  continue
208
257
 
258
+ additional_data = {
259
+ "definition_id": specs_additional_data["additional_data_definition_id"],
260
+ "value": self._convert_attribute_value(
261
+ attribute_value,
262
+ AdditionalDataDefinitionType(specs_additional_data["type"]),
263
+ attribute_name,
264
+ specs_additional_data["choices"],
265
+ log_message_suffix=log_message_suffix,
266
+ ),
267
+ }
268
+
209
269
  additional_data_list.append(additional_data)
210
270
 
211
271
  return additional_data_list
212
272
 
213
- def _to_annotation_detail_for_request(
214
- self, parser: SimpleAnnotationParser, detail: ImportedSimpleAnnotationDetail, now_datetime: str
215
- ) -> Optional[AnnotationDetailV1]:
273
+ def convert_annotation_detail(
274
+ self,
275
+ parser: SimpleAnnotationParser,
276
+ detail: ImportedSimpleAnnotationDetail,
277
+ *,
278
+ log_message_suffix: str = "",
279
+ ) -> dict[str, Any]:
216
280
  """
217
- Request Bodyに渡すDataClassに変換する。塗りつぶし画像があれば、それをS3にアップロードする。
281
+ アノテーションJSONに記載された1個のアノテーション情報を、`put_annotation` APIに渡す形式に変換する。
282
+ Outerアノテーションならば、AWS S3に外部ファイルをアップロードします。
283
+
218
284
 
219
285
  Args:
220
286
  parser:
221
- detail:
287
+ detail: インポート対象の1個のアノテーション情報
222
288
 
223
289
  Returns:
224
- 変換できない場合はNoneを返す
290
+ `put_annotation` API(v2)のリクエストボディに格納するアノテーション情報(`AnnotationDetailV2Input`)
291
+
292
+ Raises:
293
+ ValueError: 存在しないラベル名が指定された場合(`self.is_strict`がFalseでもraiseされる9
225
294
 
226
295
  """
227
- label_info = self.get_label_info_from_label_name(detail.label)
228
- if label_info is None:
229
- return None
230
-
231
- def _get_annotation_id(arg_label_info: LabelV1) -> str:
232
- if detail.annotation_id is not None:
233
- return detail.annotation_id
234
- else: # noqa: PLR5501
235
- if arg_label_info["annotation_type"] == DefaultAnnotationType.CLASSIFICATION.value:
236
- # 全体アノテーションの場合、annotation_idはlabel_idである必要がある
237
- return arg_label_info["label_id"]
238
- else:
239
- return str(uuid.uuid4())
240
-
241
- if detail.attributes is not None: # noqa: SIM108
242
- additional_data_list = self._to_additional_data_list(detail.attributes, label_info)
296
+ log_message_suffix = (
297
+ f"task_id='{parser.task_id}', input_data_id='{parser.input_data_id}', label_name='{detail.label}', annotation_id='{detail.annotation_id}'"
298
+ )
299
+
300
+ try:
301
+ label_info = self.annotation_specs_accessor.get_label(label_name=detail.label)
302
+ except ValueError:
303
+ logger.warning(
304
+ f"アノテーション仕様にラベル名(英語)が'{detail.label}'であるラベル情報が存在しないか、または複数存在します。 :: {log_message_suffix}"
305
+ )
306
+ raise
307
+
308
+ if detail.attributes is not None:
309
+ additional_data_list = self.convert_attributes(detail.attributes, label_name=detail.label, log_message_suffix=log_message_suffix)
243
310
  else:
244
311
  additional_data_list = []
245
312
 
246
- data_holding_type = self._get_data_holding_type_from_data(label_info)
247
-
248
- dest_obj = AnnotationDetailV1(
249
- label_id=label_info["label_id"],
250
- annotation_id=_get_annotation_id(label_info),
251
- account_id=self.service.api.account_id,
252
- data_holding_type=data_holding_type,
253
- data=detail.data,
254
- additional_data_list=additional_data_list,
255
- is_protected=False,
256
- etag=None,
257
- url=None,
258
- path=None,
259
- created_datetime=now_datetime,
260
- updated_datetime=now_datetime,
261
- )
313
+ result = {
314
+ "_type": "Create",
315
+ "label_id": label_info["label_id"],
316
+ "annotation_id": detail.annotation_id if detail.annotation_id is not None else create_annotation_id(label_info, self.project),
317
+ "additional_data_list": additional_data_list,
318
+ "editor_props": {},
319
+ }
262
320
 
263
- if data_holding_type == AnnotationDataHoldingType.OUTER:
321
+ if is_3dpc_segment_label(label_info):
264
322
  # TODO: 3dpc editorに依存したコード。annofab側でSimple Annotationのフォーマットが改善されたら、このコードを削除する
265
- data_uri = detail.data["data_uri"] if not self._is_3dpc_segment_label(label_info) else self._get_3dpc_segment_data_uri(detail.data)
266
- with parser.open_outer_file(data_uri) as f:
323
+ with parser.open_outer_file(get_3dpc_segment_data_uri(detail.data)) as f:
324
+ s3_path = self.service.wrapper.upload_data_to_s3(self.project_id, f, content_type="application/json")
325
+ result["body"] = {"_type": "Outer", "path": s3_path}
326
+
327
+ elif is_image_segmentation_label(label_info):
328
+ with parser.open_outer_file(detail.data["data_uri"]) as f:
267
329
  s3_path = self.service.wrapper.upload_data_to_s3(self.project_id, f, content_type="image/png")
268
- dest_obj.path = s3_path
330
+ result["body"] = {"_type": "Outer", "path": s3_path}
269
331
 
270
- return dest_obj
332
+ else:
333
+ result["body"] = {"_type": "Inner", "data": detail.data}
334
+ return result
271
335
 
272
- def parser_to_request_body(
336
+ def convert_annotation_details(
273
337
  self,
274
338
  parser: SimpleAnnotationParser,
275
339
  details: list[ImportedSimpleAnnotationDetail],
276
- old_annotation: Optional[dict[str, Any]] = None,
340
+ old_details: list[dict[str, Any]],
341
+ *,
342
+ updated_datetime: Optional[str] = None,
277
343
  ) -> dict[str, Any]:
278
- request_details: list[dict[str, Any]] = []
279
- now_datetime = str_now()
280
- for detail in details:
281
- try:
282
- request_detail = self._to_annotation_detail_for_request(parser, detail, now_datetime=now_datetime)
283
- except Exception:
284
- logger.warning(
285
- f"{parser.task_id}/{parser.input_data_id} :: アノテーションをrequest_bodyに変換するのに失敗しました。 :: "
286
- f"annotation_id='{detail.annotation_id}', label='{detail.label}'",
287
- exc_info=True,
288
- )
289
- continue
290
-
291
- if request_detail is not None:
292
- request_details.append(request_detail.to_dict(encode_json=True))
293
-
294
- updated_datetime = old_annotation["updated_datetime"] if old_annotation is not None else None
295
-
296
- request_body = {
297
- "project_id": self.project_id,
298
- "task_id": parser.task_id,
299
- "input_data_id": parser.input_data_id,
300
- "details": request_details,
301
- "updated_datetime": updated_datetime,
302
- }
303
-
304
- return request_body
344
+ """
345
+ アノテーションJSONに記載されたアノテーション情報を、`put_annotation` APIに渡す形式に変換します。
305
346
 
306
- def parser_to_request_body_with_merge(
307
- self,
308
- parser: SimpleAnnotationParser,
309
- details: list[ImportedSimpleAnnotationDetail],
310
- old_annotation: dict[str, Any],
311
- ) -> dict[str, Any]:
312
- old_details = old_annotation["details"]
347
+ Args:
348
+ parser: アノテーションJSONをパースするためのクラス
349
+ details: インポート対象のアノテーション情報
350
+ old_details: 既存のアノテーション情報。既存のアノテーション情報に加えて、インポート対象のアノテーションを登録するためのリクエストボディを作成します。
351
+ updated_datetime: 更新日時
352
+ """ # noqa: E501
313
353
  old_dict_detail = {}
314
354
  INDEX_KEY = "_index" # noqa: N806
315
355
  for index, old_detail in enumerate(old_details):
316
- if old_detail["data_holding_type"] == AnnotationDataHoldingType.OUTER.value:
317
- # 外部アノテーションを利用する際はurlが不要でpathが必要なので、対応する
318
- old_detail.pop("url", None)
356
+ old_detail.update({"_type": "Update", "body": None})
319
357
 
320
358
  # 一時的にインデックスを格納
321
359
  old_detail.update({INDEX_KEY: index})
322
360
  old_dict_detail[old_detail["annotation_id"]] = old_detail
323
361
 
324
362
  new_request_details: list[dict[str, Any]] = []
325
- now_datetime = str_now()
326
363
  for detail in details:
327
364
  try:
328
- request_detail = self._to_annotation_detail_for_request(parser, detail, now_datetime=now_datetime)
329
- except Exception:
365
+ log_message_suffix = f"task_id='{parser.task_id}', input_data_id='{parser.input_data_id}', label_name='{detail.label}', annotation_id='{detail.annotation_id}'" # noqa: E501
366
+
367
+ request_detail = self.convert_annotation_detail(parser, detail, log_message_suffix=log_message_suffix)
368
+ except Exception as e:
330
369
  logger.warning(
331
- f"{parser.task_id}/{parser.input_data_id} :: アノテーションをrequest_bodyに変換するのに失敗しました。 :: "
332
- f"annotation_id='{detail.annotation_id}', label='{detail.label}'",
333
- exc_info=True,
370
+ f"アノテーション情報を`putAnnotation`APIのリクエストボディへ変換するのに失敗しました。 :: {e!r} :: {log_message_suffix}",
334
371
  )
335
- continue
336
-
337
- if request_detail is None:
372
+ if self.is_strict:
373
+ raise
338
374
  continue
339
375
 
340
376
  if detail.annotation_id in old_dict_detail:
341
377
  # アノテーションを上書き
342
378
  old_detail = old_dict_detail[detail.annotation_id]
343
- old_details[old_detail[INDEX_KEY]] = request_detail.to_dict(encode_json=True)
379
+ request_detail["_type"] = "Update"
380
+ old_details[old_detail[INDEX_KEY]] = request_detail
344
381
  else:
345
382
  # アノテーションの追加
346
- new_request_details.append(request_detail.to_dict(encode_json=True))
383
+ new_request_details.append(request_detail)
347
384
 
348
385
  new_details = old_details + new_request_details
349
386
 
@@ -352,11 +389,35 @@ class ImportAnnotationMain(CommandLineWithConfirm):
352
389
  "task_id": parser.task_id,
353
390
  "input_data_id": parser.input_data_id,
354
391
  "details": new_details,
355
- "updated_datetime": old_annotation["updated_datetime"],
392
+ "updated_datetime": updated_datetime,
393
+ "format_version": "2.0.0",
356
394
  }
357
395
 
358
396
  return request_body
359
397
 
398
+
399
+ class ImportAnnotationMain(CommandLineWithConfirm):
400
+ def __init__(
401
+ self,
402
+ service: annofabapi.Resource,
403
+ *,
404
+ project_id: str,
405
+ all_yes: bool,
406
+ is_force: bool,
407
+ is_merge: bool,
408
+ is_overwrite: bool,
409
+ converter: AnnotationConverter,
410
+ ) -> None:
411
+ self.service = service
412
+ self.facade = AnnofabApiFacade(service)
413
+ CommandLineWithConfirm.__init__(self, all_yes)
414
+
415
+ self.project_id = project_id
416
+ self.is_force = is_force
417
+ self.is_merge = is_merge
418
+ self.is_overwrite = is_overwrite
419
+ self.converter = converter
420
+
360
421
  def put_annotation_for_input_data(self, parser: SimpleAnnotationParser) -> bool:
361
422
  task_id = parser.task_id
362
423
  input_data_id = parser.input_data_id
@@ -364,32 +425,36 @@ class ImportAnnotationMain(CommandLineWithConfirm):
364
425
  simple_annotation: ImportedSimpleAnnotation = ImportedSimpleAnnotation.from_dict(parser.load_json())
365
426
  if len(simple_annotation.details) == 0:
366
427
  logger.debug(
367
- f"task_id={task_id}, input_data_id={input_data_id} : インポート元にアノテーションデータがないため、アノテーションの登録をスキップします。" # noqa: E501
428
+ f"task_id='{task_id}', input_data_id='{input_data_id}' :: インポート元にアノテーションデータがないため、アノテーションの登録をスキップします。" # noqa: E501
368
429
  )
369
430
  return False
370
431
 
371
432
  input_data = self.service.wrapper.get_input_data_or_none(self.project_id, input_data_id)
372
433
  if input_data is None:
373
- logger.warning(f"task_id= '{task_id}, input_data_id = '{input_data_id}' は存在しません。")
434
+ logger.warning(f"input_data_id='{input_data_id}'という入力データは存在しません。 :: task_id='{task_id}'")
374
435
  return False
375
436
 
376
- old_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id)
437
+ old_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id, query_params={"v": "2"})
377
438
  if len(old_annotation["details"]) > 0: # noqa: SIM102
378
439
  if not self.is_overwrite and not self.is_merge:
379
440
  logger.debug(
380
- f"task_id={task_id}, input_data_id={input_data_id} : "
381
- f"インポート先のタスクに既にアノテーションが存在するため、アノテーションの登録をスキップします。"
441
+ f"task_id='{task_id}', input_data_id='{input_data_id}' :: "
442
+ f"インポート先のタスク内の入力データに既にアノテーションが存在するため、アノテーションの登録をスキップします。"
382
443
  f"アノテーションをインポートする場合は、`--overwrite` または '--merge' を指定してください。"
383
444
  )
384
445
  return False
385
446
 
386
447
  logger.info(f"task_id='{task_id}', input_data_id='{input_data_id}' :: {len(simple_annotation.details)} 件のアノテーションを登録します。")
387
448
  if self.is_merge:
388
- request_body = self.parser_to_request_body_with_merge(parser, simple_annotation.details, old_annotation=old_annotation)
449
+ request_body = self.converter.convert_annotation_details(
450
+ parser, simple_annotation.details, old_details=old_annotation["details"], updated_datetime=old_annotation["updated_datetime"]
451
+ )
389
452
  else:
390
- request_body = self.parser_to_request_body(parser, simple_annotation.details, old_annotation=old_annotation)
453
+ request_body = self.converter.convert_annotation_details(
454
+ parser, simple_annotation.details, old_details=[], updated_datetime=old_annotation["updated_datetime"]
455
+ )
391
456
 
392
- self.service.api.put_annotation(self.project_id, task_id, input_data_id, request_body=request_body)
457
+ self.service.api.put_annotation(self.project_id, task_id, input_data_id, request_body=request_body, query_params={"v": "2"})
393
458
  return True
394
459
 
395
460
  def put_annotation_for_task(self, task_parser: SimpleAnnotationParserByTask) -> int:
@@ -418,15 +483,15 @@ class ImportAnnotationMain(CommandLineWithConfirm):
418
483
 
419
484
  """
420
485
  task_id = task_parser.task_id
421
- if not self.confirm_processing(f"task_id={task_id} のアノテーションをインポートしますか?"):
486
+ if not self.confirm_processing(f"task_id='{task_id}' のアノテーションをインポートしますか?"):
422
487
  return False
423
488
 
424
489
  logger_prefix = f"{task_index + 1!s} 件目: " if task_index is not None else ""
425
- logger.info(f"{logger_prefix}task_id={task_id} に対して処理します。")
490
+ logger.info(f"{logger_prefix}task_id='{task_id}' に対して処理します。")
426
491
 
427
492
  task = self.service.wrapper.get_task_or_none(self.project_id, task_id)
428
493
  if task is None:
429
- logger.warning(f"task_id = '{task_id}' は存在しません。")
494
+ logger.warning(f"task_id='{task_id}'であるタスクは存在しません。")
430
495
  return False
431
496
 
432
497
  if task["status"] in [TaskStatus.WORKING.value, TaskStatus.COMPLETE.value]:
@@ -582,6 +647,10 @@ class ImportAnnotation(CommandLine):
582
647
  logger.warning(f"annotation_path: '{annotation_path}' は、zipファイルまたはディレクトリではありませんでした。")
583
648
  return
584
649
 
650
+ annotation_specs_v3, _ = self.service.api.get_annotation_specs(project_id, query_params={"v": "3"})
651
+ project, _ = self.service.api.get_project(project_id)
652
+ converter = AnnotationConverter(project=project, annotation_specs=annotation_specs_v3, is_strict=args.strict, service=self.service)
653
+
585
654
  main_obj = ImportAnnotationMain(
586
655
  self.service,
587
656
  project_id=project_id,
@@ -589,6 +658,7 @@ class ImportAnnotation(CommandLine):
589
658
  is_merge=args.merge,
590
659
  is_overwrite=args.overwrite,
591
660
  is_force=args.force,
661
+ converter=converter,
592
662
  )
593
663
 
594
664
  main_obj.main(iter_task_parser, target_task_ids=target_task_ids, parallelism=args.parallelism)
@@ -638,6 +708,13 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
638
708
  help="過去に割り当てられていて現在の担当者が自分自身でない場合、タスクの担当者を自分自身に変更してからアノテーションをインポートします。",
639
709
  )
640
710
 
711
+ parser.add_argument(
712
+ "--strict",
713
+ action="store_true",
714
+ help="アノテーションJSONに、存在しないラベル名や属性名など適切でない記載が場合、そのアノテーションJSONの登録をスキップします。"
715
+ "デフォルトでは、適切でない箇所のみスキップして、できるだけアノテーションを登録するようにします。",
716
+ )
717
+
641
718
  parser.add_argument(
642
719
  "--parallelism",
643
720
  type=int,