Skip to content

Commit e55dfca

Browse files
authored
Merge branch 'main' into 20260515-6766
2 parents 38a4847 + c4be661 commit e55dfca

20 files changed

Lines changed: 1805 additions & 1271 deletions

annofabcli/annotation_specs/add_attribute_restriction.py

Lines changed: 109 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import json
66
import logging
77
import sys
8+
from dataclasses import dataclass
89
from typing import Any
910

1011
import annofabapi
@@ -23,13 +24,100 @@
2324
logger = logging.getLogger(__name__)
2425

2526

27+
@dataclass(frozen=True)
28+
class ResolvedRestrictionsForAdd:
29+
"""
30+
追加対象の属性制約を解決した結果。
31+
"""
32+
33+
new_restrictions: list[dict[str, Any]]
34+
"""追加する属性制約一覧。"""
35+
36+
new_restriction_text_list: list[str]
37+
"""追加する属性制約のテキスト一覧。"""
38+
39+
2640
def create_comment_from_restriction_text(restriction_text_list: list[str]) -> str:
2741
"""
2842
属性制約のテキストから、アノテーション仕様変更時のコメントを生成する
2943
"""
3044
return "以下の属性制約を追加しました。\n" + "\n".join(restriction_text_list)
3145

3246

47+
def resolve_restrictions_to_add(
48+
annotation_specs: dict[str, Any],
49+
restrictions: list[dict[str, Any]],
50+
) -> ResolvedRestrictionsForAdd:
51+
"""
52+
追加対象の属性制約を既存アノテーション仕様に対して解決する。
53+
54+
Args:
55+
annotation_specs: 既存のアノテーション仕様
56+
restrictions: 追加候補の属性制約一覧
57+
58+
Returns:
59+
解決済み追加対象
60+
"""
61+
old_restrictions = annotation_specs["restrictions"]
62+
msg_obj = AttributeRestrictionMessage(
63+
labels=annotation_specs["labels"],
64+
additionals=annotation_specs["additionals"],
65+
raise_if_not_found=True,
66+
)
67+
68+
new_restrictions = []
69+
new_restriction_text_list = []
70+
for index, restriction in enumerate(restrictions):
71+
try:
72+
restriction_text = msg_obj.get_restriction_text(restriction["additional_data_definition_id"], restriction["condition"])
73+
except ValueError as e:
74+
logger.warning(f"{index + 1}件目 :: 次の属性制約は存在しないIDが含まれていたため、アノテーション仕様に追加しません。 :: restriction=`{restriction}`, error_message=`{e!s}`")
75+
continue
76+
77+
if restriction in old_restrictions:
78+
logger.warning(f"{index + 1}件目 :: 次の属性制約は既に存在するため、アノテーション仕様に追加しません。 :: `{restriction_text}`")
79+
continue
80+
if restriction in new_restrictions:
81+
logger.warning(f"{index + 1}件目 :: 次の属性制約は入力内で重複しているため、アノテーション仕様に追加しません。 :: `{restriction_text}`")
82+
continue
83+
84+
new_restrictions.append(restriction)
85+
new_restriction_text_list.append(restriction_text)
86+
87+
return ResolvedRestrictionsForAdd(
88+
new_restrictions=new_restrictions,
89+
new_restriction_text_list=new_restriction_text_list,
90+
)
91+
92+
93+
def build_request_body_for_add_attribute_restriction(
94+
annotation_specs: dict[str, Any],
95+
*,
96+
new_restrictions: list[dict[str, Any]],
97+
new_restriction_text_list: list[str],
98+
comment: str | None,
99+
) -> dict[str, Any]:
100+
"""
101+
属性制約追加用の request body を生成する。
102+
103+
Args:
104+
annotation_specs: 既存のアノテーション仕様
105+
new_restrictions: 追加する属性制約一覧
106+
new_restriction_text_list: 追加する属性制約のテキスト一覧
107+
comment: 変更コメント
108+
109+
Returns:
110+
Annofab API に渡す request body
111+
"""
112+
request_body = copy.deepcopy(annotation_specs)
113+
request_body["restrictions"].extend(new_restrictions)
114+
if comment is None:
115+
comment = create_comment_from_restriction_text(new_restriction_text_list)
116+
request_body["comment"] = comment
117+
request_body["last_updated_datetime"] = annotation_specs["updated_datetime"]
118+
return request_body
119+
120+
33121
class AddAttributeRestrictionMain(CommandLineWithConfirm):
34122
def __init__(
35123
self,
@@ -44,52 +132,37 @@ def __init__(
44132

45133
def add_restrictions(self, restrictions: list[dict[str, Any]], comment: str | None = None) -> bool:
46134
old_annotation_specs, _ = self.service.api.get_annotation_specs(self.project_id, query_params={"v": "3"})
47-
old_restrictions = old_annotation_specs["restrictions"]
48-
49-
msg_obj = AttributeRestrictionMessage(
50-
labels=old_annotation_specs["labels"],
51-
additionals=old_annotation_specs["additionals"],
52-
raise_if_not_found=True,
53-
)
54-
55-
new_restrictions = []
56-
new_restriction_text_list = []
57-
for index, restriction in enumerate(restrictions):
58-
try:
59-
restriction_text = msg_obj.get_restriction_text(restriction["additional_data_definition_id"], restriction["condition"])
60-
except ValueError as e:
61-
logger.warning(f"{index + 1}件目 :: 次の属性制約は存在しないIDが含まれていたため、アノテーション仕様に追加しません。 :: restriction=`{restriction}`, error_message=`{e!s}`")
62-
continue
63-
64-
if restriction in old_restrictions:
65-
logger.warning(f"{index + 1}件目 :: 次の属性制約は既に存在するため、アノテーション仕様に追加しません。 :: `{restriction_text}`")
66-
continue
67-
if restriction in new_restrictions:
68-
logger.warning(f"{index + 1}件目 :: 次の属性制約は入力内で重複しているため、アノテーション仕様に追加しません。 :: `{restriction_text}`")
69-
continue
70-
135+
resolved_restrictions = resolve_restrictions_to_add(old_annotation_specs, restrictions)
136+
137+
confirmed_restrictions = []
138+
confirmed_restriction_text_list = []
139+
for restriction, restriction_text in zip(
140+
resolved_restrictions.new_restrictions,
141+
resolved_restrictions.new_restriction_text_list,
142+
strict=True,
143+
):
71144
if not self.confirm_processing(f"次の属性制約を追加しますか? :: `{restriction_text}`"):
72145
continue
73146

74-
logger.debug(f"{index + 1}件目 :: 次の属性制約を追加します。 :: `{restriction_text}`")
75-
new_restrictions.append(restriction)
76-
new_restriction_text_list.append(restriction_text)
147+
logger.debug(f"次の属性制約を追加します。 :: `{restriction_text}`")
148+
confirmed_restrictions.append(restriction)
149+
confirmed_restriction_text_list.append(restriction_text)
77150

78-
if len(new_restrictions) == 0:
151+
if len(confirmed_restrictions) == 0:
79152
logger.info("追加する属性制約はないため、アノテーション仕様を変更しません。")
80153
return False
81154

82-
if not self.confirm_processing(f"{len(new_restrictions)} 件の属性制約を追加して、アノテーション仕様を変更します。よろしいですか? "):
155+
if not self.confirm_processing(f"{len(confirmed_restrictions)} 件の属性制約を追加して、アノテーション仕様を変更します。よろしいですか? "):
83156
return False
84157

85-
request_body = copy.deepcopy(old_annotation_specs)
86-
request_body["restrictions"].extend(new_restrictions)
87-
if comment is None:
88-
comment = create_comment_from_restriction_text(new_restriction_text_list)
89-
request_body["comment"] = comment
90-
request_body["last_updated_datetime"] = old_annotation_specs["updated_datetime"]
158+
request_body = build_request_body_for_add_attribute_restriction(
159+
old_annotation_specs,
160+
new_restrictions=confirmed_restrictions,
161+
new_restriction_text_list=confirmed_restriction_text_list,
162+
comment=comment,
163+
)
91164
self.service.api.put_annotation_specs(self.project_id, query_params={"v": "3"}, request_body=request_body)
92-
logger.info(f"{len(new_restrictions)} 件の属性制約をアノテーション仕様に追加しました。")
165+
logger.info(f"{len(confirmed_restrictions)} 件の属性制約をアノテーション仕様に追加しました。")
93166
return True
94167

95168

annofabcli/annotation_specs/add_choice_attribute.py

Lines changed: 116 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import json
66
import logging
77
import uuid
8-
from collections.abc import Sequence
8+
from collections.abc import Mapping, Sequence
99
from dataclasses import dataclass
1010
from pathlib import Path
1111
from typing import Any
@@ -49,6 +49,22 @@ class ChoiceAttributeInput:
4949
"""属性のデフォルト値として使用する選択肢かどうか"""
5050

5151

52+
@dataclass(frozen=True)
53+
class ResolvedChoiceAttributeInput:
54+
"""
55+
既存アノテーション仕様に対して解決済みの選択肢系属性入力。
56+
"""
57+
58+
target_labels: list[Mapping[str, Any]]
59+
"""属性を追加する対象ラベル一覧。"""
60+
61+
new_attribute: dict[str, Any]
62+
"""Annofab API向けの新規属性オブジェクト。"""
63+
64+
duplicated_name_attribute_ids: list[str]
65+
"""同名属性の既存属性ID一覧。"""
66+
67+
5268
def parse_choice_input_from_dict(data: dict[str, Any], *, index: int) -> ChoiceAttributeInput:
5369
"""
5470
JSONオブジェクト1件を選択肢入力に変換する。
@@ -287,6 +303,88 @@ def create_attribute(
287303
}
288304

289305

306+
def resolve_choice_attribute_input(
307+
annotation_specs: dict[str, Any],
308+
*,
309+
attribute_type: str,
310+
attribute_name_en: str,
311+
attribute_name_ja: str | None,
312+
attribute_id: str | None,
313+
choice_inputs: Sequence[ChoiceAttributeInput],
314+
label_ids: Sequence[str] | None,
315+
label_name_ens: Sequence[str] | None,
316+
) -> ResolvedChoiceAttributeInput:
317+
"""
318+
選択肢系属性入力を既存アノテーション仕様に対して解決する。
319+
320+
Args:
321+
annotation_specs: 既存のアノテーション仕様
322+
attribute_type: 属性型。 ``choice`` または ``select``
323+
attribute_name_en: 属性英語名
324+
attribute_name_ja: 属性日本語名
325+
attribute_id: 属性ID。未指定ならUUIDv4を自動生成
326+
choice_inputs: 選択肢入力一覧
327+
label_ids: 追加先ラベルID一覧。未指定時はNone
328+
label_name_ens: 追加先ラベル英語名一覧。未指定時はNone
329+
330+
Returns:
331+
解決済み選択肢系属性入力
332+
"""
333+
annotation_specs_accessor = AnnotationSpecsAccessor(annotation_specs)
334+
target_labels = get_target_labels(annotation_specs_accessor, label_ids=label_ids, label_name_ens=label_name_ens)
335+
new_attribute = create_attribute(
336+
attribute_type=attribute_type,
337+
attribute_name_en=attribute_name_en,
338+
attribute_name_ja=attribute_name_ja,
339+
attribute_id=attribute_id,
340+
choice_inputs=choice_inputs,
341+
)
342+
duplicated_name_attribute_ids = validate_new_attribute(
343+
annotation_specs["additionals"],
344+
attribute_id=new_attribute["additional_data_definition_id"],
345+
attribute_name_en=attribute_name_en,
346+
)
347+
return ResolvedChoiceAttributeInput(
348+
target_labels=target_labels,
349+
new_attribute=new_attribute,
350+
duplicated_name_attribute_ids=duplicated_name_attribute_ids,
351+
)
352+
353+
354+
def build_request_body_for_add_choice_attribute(
355+
annotation_specs: dict[str, Any],
356+
*,
357+
resolved_choice_attribute_input: ResolvedChoiceAttributeInput,
358+
attribute_name_en: str,
359+
comment: str | None,
360+
) -> dict[str, Any]:
361+
"""
362+
選択肢系属性追加用の request body を生成する。
363+
364+
Args:
365+
annotation_specs: 既存のアノテーション仕様
366+
resolved_choice_attribute_input: 解決済み選択肢系属性入力
367+
attribute_name_en: 属性英語名
368+
comment: 変更コメント
369+
370+
Returns:
371+
Annofab API に渡す request body
372+
"""
373+
request_body = copy.deepcopy(annotation_specs)
374+
request_body["additionals"].append(resolved_choice_attribute_input.new_attribute)
375+
target_label_id_set = {label["label_id"] for label in resolved_choice_attribute_input.target_labels}
376+
for label in request_body["labels"]:
377+
if label["label_id"] in target_label_id_set:
378+
label["additional_data_definitions"].append(resolved_choice_attribute_input.new_attribute["additional_data_definition_id"])
379+
380+
label_names = [get_label_name_en(label) for label in resolved_choice_attribute_input.target_labels]
381+
if comment is None:
382+
comment = create_comment_from_attribute(attribute_name_en, label_names)
383+
request_body["comment"] = comment
384+
request_body["last_updated_datetime"] = annotation_specs["updated_datetime"]
385+
return request_body
386+
387+
290388
class AddChoiceAttributeMain(CommandLineWithConfirm):
291389
"""
292390
選択肢系属性をアノテーション仕様へ追加する本体処理。
@@ -335,44 +433,36 @@ def add_choice_attribute(
335433
ValueError: 入力値や既存アノテーション仕様との整合性が不正な場合
336434
"""
337435
old_annotation_specs, _ = self.service.api.get_annotation_specs(self.project_id, query_params={"v": "3"})
338-
annotation_specs_accessor = AnnotationSpecsAccessor(old_annotation_specs)
339-
additionals = old_annotation_specs["additionals"]
340-
target_labels = get_target_labels(annotation_specs_accessor, label_ids=label_ids, label_name_ens=label_name_ens)
341-
new_attribute = create_attribute(
436+
resolved_choice_attribute_input = resolve_choice_attribute_input(
437+
old_annotation_specs,
342438
attribute_type=attribute_type,
343439
attribute_name_en=attribute_name_en,
344440
attribute_name_ja=attribute_name_ja,
345441
attribute_id=attribute_id,
346442
choice_inputs=choice_inputs,
443+
label_ids=label_ids,
444+
label_name_ens=label_name_ens,
347445
)
348446

349-
duplicated_name_attribute_ids = validate_new_attribute(
350-
additionals,
351-
attribute_id=new_attribute["additional_data_definition_id"],
352-
attribute_name_en=attribute_name_en,
447+
label_names = [get_label_name_en(label) for label in resolved_choice_attribute_input.target_labels]
448+
confirm_message = (
449+
f"属性名(英語)='{attribute_name_en}', 属性種類='{attribute_type}', "
450+
f"選択肢数={len(resolved_choice_attribute_input.new_attribute['choices'])}, 対象ラベル={label_names} を追加します。よろしいですか?"
353451
)
354-
355-
label_names = [get_label_name_en(label) for label in target_labels]
356-
confirm_message = f"属性名(英語)='{attribute_name_en}', 属性種類='{attribute_type}', 選択肢数={len(new_attribute['choices'])}, 対象ラベル={label_names} を追加します。よろしいですか?"
357-
if duplicated_name_attribute_ids:
358-
duplicated_text = ", ".join(duplicated_name_attribute_ids)
452+
if resolved_choice_attribute_input.duplicated_name_attribute_ids:
453+
duplicated_text = ", ".join(resolved_choice_attribute_input.duplicated_name_attribute_ids)
359454
confirm_message = f"{confirm_message} なお、同じ属性名(英語)を持つ既存属性があります。既存の属性ID: {duplicated_text}"
360455
if not self.confirm_processing(confirm_message):
361456
return False
362457

363-
request_body = copy.deepcopy(old_annotation_specs)
364-
request_body["additionals"].append(new_attribute)
365-
target_label_id_set = {label["label_id"] for label in target_labels}
366-
for label in request_body["labels"]:
367-
if label["label_id"] in target_label_id_set:
368-
label["additional_data_definitions"].append(new_attribute["additional_data_definition_id"])
369-
370-
if comment is None:
371-
comment = create_comment_from_attribute(attribute_name_en, label_names)
372-
request_body["comment"] = comment
373-
request_body["last_updated_datetime"] = old_annotation_specs["updated_datetime"]
458+
request_body = build_request_body_for_add_choice_attribute(
459+
old_annotation_specs,
460+
resolved_choice_attribute_input=resolved_choice_attribute_input,
461+
attribute_name_en=attribute_name_en,
462+
comment=comment,
463+
)
374464
self.service.api.put_annotation_specs(self.project_id, query_params={"v": "3"}, request_body=request_body)
375-
logger.info(f"属性名(英語)='{attribute_name_en}', 属性ID='{new_attribute['additional_data_definition_id']}' の選択肢系属性を追加しました。")
465+
logger.info(f"属性名(英語)='{attribute_name_en}', 属性ID='{resolved_choice_attribute_input.new_attribute['additional_data_definition_id']}' の選択肢系属性を追加しました。")
376466
return True
377467

378468

0 commit comments

Comments
 (0)