annofabcli 1.98.0__py3-none-any.whl → 1.100.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- annofabcli/__version__.py +1 -1
- annofabcli/annotation_specs/add_attribute_restriction.py +158 -0
- annofabcli/annotation_specs/attribute_restriction.py +240 -0
- annofabcli/annotation_specs/list_attribute_restriction.py +8 -209
- annofabcli/annotation_specs/put_label_color.py +5 -0
- annofabcli/annotation_specs/subcommand_annotation_specs.py +2 -0
- annofabcli/comment/delete_comment.py +3 -0
- annofabcli/comment/put_inspection_comment.py +3 -0
- annofabcli/comment/put_onhold_comment.py +3 -0
- annofabcli/input_data/change_input_data_name.py +4 -1
- annofabcli/input_data/put_input_data.py +4 -0
- annofabcli/statistics/list_annotation_attribute_filled_count.py +684 -0
- annofabcli/statistics/list_annotation_count.py +80 -37
- annofabcli/statistics/list_annotation_duration.py +18 -18
- annofabcli/statistics/subcommand_statistics.py +3 -0
- annofabcli/task/list_tasks_added_task_history.py +141 -156
- annofabcli/task/put_tasks.py +9 -4
- {annofabcli-1.98.0.dist-info → annofabcli-1.100.0.dist-info}/METADATA +1 -1
- {annofabcli-1.98.0.dist-info → annofabcli-1.100.0.dist-info}/RECORD +22 -19
- {annofabcli-1.98.0.dist-info → annofabcli-1.100.0.dist-info}/LICENSE +0 -0
- {annofabcli-1.98.0.dist-info → annofabcli-1.100.0.dist-info}/WHEEL +0 -0
- {annofabcli-1.98.0.dist-info → annofabcli-1.100.0.dist-info}/entry_points.txt +0 -0
annofabcli/__version__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.
|
|
1
|
+
__version__ = "1.100.0" # `poetry-dynamic-versioning`を使ってGitHubのバージョンタグを取得している。変更不要
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import copy
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
import annofabapi
|
|
11
|
+
|
|
12
|
+
import annofabcli
|
|
13
|
+
import annofabcli.common.cli
|
|
14
|
+
from annofabcli.annotation_specs.attribute_restriction import AttributeRestrictionMessage, OutputFormat
|
|
15
|
+
from annofabcli.common.cli import (
|
|
16
|
+
COMMAND_LINE_ERROR_STATUS_CODE,
|
|
17
|
+
CommandLine,
|
|
18
|
+
CommandLineWithConfirm,
|
|
19
|
+
build_annofabapi_resource_and_login,
|
|
20
|
+
get_json_from_args,
|
|
21
|
+
)
|
|
22
|
+
from annofabcli.common.facade import AnnofabApiFacade
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def create_comment_from_restriction_text(restriction_text_list: list[str]) -> str:
|
|
28
|
+
"""
|
|
29
|
+
属性制約のテキストから、アノテーション仕様変更時のコメントを生成する
|
|
30
|
+
"""
|
|
31
|
+
return "以下の属性制約を追加しました。\n" + "\n".join(restriction_text_list)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AddAttributeRestrictionMain(CommandLineWithConfirm):
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
service: annofabapi.Resource,
|
|
38
|
+
*,
|
|
39
|
+
project_id: str,
|
|
40
|
+
all_yes: bool,
|
|
41
|
+
) -> None:
|
|
42
|
+
self.service = service
|
|
43
|
+
CommandLineWithConfirm.__init__(self, all_yes)
|
|
44
|
+
self.project_id = project_id
|
|
45
|
+
|
|
46
|
+
def add_restrictions(self, restrictions: list[dict[str, Any]], comment: Optional[str] = None) -> bool:
|
|
47
|
+
old_annotation_specs, _ = self.service.api.get_annotation_specs(self.project_id, query_params={"v": "3"})
|
|
48
|
+
old_restrictions = old_annotation_specs["restrictions"]
|
|
49
|
+
|
|
50
|
+
msg_obj = AttributeRestrictionMessage(
|
|
51
|
+
labels=old_annotation_specs["labels"],
|
|
52
|
+
additionals=old_annotation_specs["additionals"],
|
|
53
|
+
output_format=OutputFormat.TEXT,
|
|
54
|
+
raise_if_not_found=True,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
new_restrictions = []
|
|
58
|
+
new_restriction_text_list = []
|
|
59
|
+
for index, restriction in enumerate(restrictions):
|
|
60
|
+
try:
|
|
61
|
+
restriction_text = msg_obj.get_restriction_text(restriction["additional_data_definition_id"], restriction["condition"])
|
|
62
|
+
except ValueError as e:
|
|
63
|
+
logger.warning(
|
|
64
|
+
f"{index + 1}件目 :: 次の属性制約は存在しないIDが含まれていたため、アノテーション仕様に追加しません。 :: "
|
|
65
|
+
f"restriction=`{restriction}`, error_message=`{e!s}`"
|
|
66
|
+
)
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
if restriction in old_restrictions:
|
|
70
|
+
logger.warning(f"{index + 1}件目 :: 次の属性制約は既に存在するため、アノテーション仕様に追加しません。 :: `{restriction_text}`")
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
if not self.confirm_processing(f"次の属性制約を追加しますか? :: `{restriction_text}`"):
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
logger.debug(f"{index + 1}件目 :: 次の属性制約を追加します。 :: `{restriction_text}`")
|
|
77
|
+
new_restrictions.append(restriction)
|
|
78
|
+
new_restriction_text_list.append(restriction_text)
|
|
79
|
+
|
|
80
|
+
if len(new_restrictions) == 0:
|
|
81
|
+
logger.info("追加する属性制約はないため、アノテーション仕様を変更しません。")
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
if not self.confirm_processing(f"{len(new_restrictions)} 件の属性制約を追加して、アノテーション仕様を変更します。よろしいですか? "):
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
request_body = copy.deepcopy(old_annotation_specs)
|
|
88
|
+
request_body["restrictions"].extend(restrictions)
|
|
89
|
+
if comment is None:
|
|
90
|
+
comment = create_comment_from_restriction_text(new_restriction_text_list)
|
|
91
|
+
request_body["comment"] = comment
|
|
92
|
+
request_body["last_updated_datetime"] = old_annotation_specs["updated_datetime"]
|
|
93
|
+
self.service.api.put_annotation_specs(self.project_id, query_params={"v": "3"}, request_body=request_body)
|
|
94
|
+
logger.info(f"{len(new_restrictions)} 件の属性制約をアノテーション仕様に追加しました。")
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class AddAttributeRestriction(CommandLine):
|
|
99
|
+
COMMON_MESSAGE = "annofabcli annotation_specs add_restriction: error:"
|
|
100
|
+
|
|
101
|
+
def get_history_id_from_before_index(self, project_id: str, before: int) -> Optional[str]:
|
|
102
|
+
histories, _ = self.service.api.get_annotation_specs_histories(project_id)
|
|
103
|
+
if before + 1 > len(histories):
|
|
104
|
+
logger.warning(f"アノテーション仕様の履歴は{len(histories)}個のため、最新より{before}個前のアノテーション仕様は見つかりませんでした。")
|
|
105
|
+
return None
|
|
106
|
+
history = histories[-(before + 1)]
|
|
107
|
+
return history["history_id"]
|
|
108
|
+
|
|
109
|
+
def main(self) -> None:
|
|
110
|
+
args = self.args
|
|
111
|
+
|
|
112
|
+
restrictions = get_json_from_args(args.json)
|
|
113
|
+
if not isinstance(restrictions, list):
|
|
114
|
+
print(f"{self.COMMON_MESSAGE}: error: JSON形式が不正です。オブジェクトの配列を指定してください。", file=sys.stderr) # noqa: T201
|
|
115
|
+
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
116
|
+
|
|
117
|
+
obj = AddAttributeRestrictionMain(self.service, project_id=args.project_id, all_yes=args.yes)
|
|
118
|
+
obj.add_restrictions(restrictions, comment=args.comment)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
122
|
+
parser.add_argument("-p", "--project_id", help="対象のプロジェクトのproject_idを指定します。", required=True)
|
|
123
|
+
|
|
124
|
+
sample_json = [
|
|
125
|
+
{
|
|
126
|
+
"additional_data_definition_id": "a1",
|
|
127
|
+
"condition": {"value": "true", "_type": "Equals"},
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
parser.add_argument(
|
|
131
|
+
"--json",
|
|
132
|
+
type=str,
|
|
133
|
+
required=True,
|
|
134
|
+
help="追加する属性の制約情報のJSONを指定します。"
|
|
135
|
+
"JSON形式は ... を参照してください。\n"
|
|
136
|
+
f"(例) ``{json.dumps(sample_json)}``\n"
|
|
137
|
+
"``file://`` を先頭に付けるとjsonファイルを指定できます。",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
parser.add_argument("--comment", type=str, help="アノテーション仕様の変更時に指定できるコメント。未指定の場合、自動でコメントが生成されます。")
|
|
141
|
+
|
|
142
|
+
parser.set_defaults(subcommand_func=main)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def main(args: argparse.Namespace) -> None:
|
|
146
|
+
service = build_annofabapi_resource_and_login(args)
|
|
147
|
+
facade = AnnofabApiFacade(service)
|
|
148
|
+
AddAttributeRestriction(service, facade, args).main()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
|
|
152
|
+
subcommand_name = "add_attribute_restriction"
|
|
153
|
+
|
|
154
|
+
subcommand_help = "アノテーション仕様に属性の制約を追加します。"
|
|
155
|
+
description = subcommand_help + "アノテーション仕様画面では設定できない「属性間の制約」を追加するときに有用です。"
|
|
156
|
+
parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description=description)
|
|
157
|
+
parse_args(parser)
|
|
158
|
+
return parser
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import Collection
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from more_itertools import first_true
|
|
7
|
+
|
|
8
|
+
from annofabcli.common.facade import AnnofabApiFacade
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OutputFormat(Enum):
|
|
14
|
+
"""
|
|
15
|
+
表示するフォーマット ``--format`` で指定できる値
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
TEXT: 属性IDや種類を隠したシンプルなテキスト
|
|
19
|
+
DETAILED_TEXT: 属性IDや属性種類などの詳細情報を表示したテキスト
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
TEXT = "text"
|
|
23
|
+
DETAILED_TEXT = "detailed_text"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AttributeRestrictionMessage:
|
|
27
|
+
"""
|
|
28
|
+
アノテーション仕様の属性制約情報から、自然言語で書かれたメッセージを生成する
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
labels: アノテーション仕様のラベル情報
|
|
32
|
+
additionals: アノテーション仕様の属性情報
|
|
33
|
+
raise_if_not_found: 属性やラベルが見つからなかった場合に例外を発生させるかどうか
|
|
34
|
+
format: 属性制約の表示フォーマット
|
|
35
|
+
- `text`: 属性IDを隠したシンプルなテキスト
|
|
36
|
+
- `detailed_text`: 属性IDなどの詳細情報を表示したテキスト
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
labels: list[dict[str, Any]],
|
|
43
|
+
additionals: list[dict[str, Any]],
|
|
44
|
+
*,
|
|
45
|
+
raise_if_not_found: bool = False,
|
|
46
|
+
output_format: OutputFormat = OutputFormat.DETAILED_TEXT, # pylint: disable=redefined-builtin
|
|
47
|
+
) -> None:
|
|
48
|
+
self.attribute_dict = {e["additional_data_definition_id"]: e for e in additionals}
|
|
49
|
+
self.label_dict = {e["label_id"]: e for e in labels}
|
|
50
|
+
self.output_format = output_format
|
|
51
|
+
self.raise_if_not_found = raise_if_not_found
|
|
52
|
+
|
|
53
|
+
def get_labels_text(self, label_ids: Collection[str]) -> str:
|
|
54
|
+
label_message_list = []
|
|
55
|
+
for label_id in label_ids:
|
|
56
|
+
label = self.label_dict.get(label_id)
|
|
57
|
+
if label is not None:
|
|
58
|
+
label_name = AnnofabApiFacade.get_label_name_en(label)
|
|
59
|
+
else:
|
|
60
|
+
logger.warning(f"ラベルIDが'{label_id}'であるラベルは存在しません。")
|
|
61
|
+
if self.raise_if_not_found:
|
|
62
|
+
raise ValueError(f"ラベルIDが'{label_id}'であるラベルは存在しません。")
|
|
63
|
+
label_name = ""
|
|
64
|
+
|
|
65
|
+
label_message = f"'{label_name}'"
|
|
66
|
+
if self.output_format == OutputFormat.DETAILED_TEXT:
|
|
67
|
+
label_message = f"{label_message} (id='{label_id}')"
|
|
68
|
+
label_message_list.append(label_message)
|
|
69
|
+
|
|
70
|
+
return ", ".join(label_message_list)
|
|
71
|
+
|
|
72
|
+
def get_object_for_equals_or_notequals(self, value: str, attribute: Optional[dict[str, Any]]) -> str:
|
|
73
|
+
"""制約条件が `Equals` or `NotEquals`のときの目的語を生成する。
|
|
74
|
+
属性の種類がドロップダウンかセレクトボックスのときは、選択肢の名前を返す。
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
value (str): _description_
|
|
78
|
+
attribute (Optional[dict[str,Any]]): _description_
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
str: _description_
|
|
82
|
+
"""
|
|
83
|
+
if attribute is not None and attribute["type"] in ["choice", "select"]:
|
|
84
|
+
# ラジオボタンかドロップダウンのとき
|
|
85
|
+
choices = attribute["choices"]
|
|
86
|
+
choice = first_true(choices, pred=lambda e: e["choice_id"] == value)
|
|
87
|
+
if choice is not None:
|
|
88
|
+
choice_name = AnnofabApiFacade.get_choice_name_en(choice)
|
|
89
|
+
tmp = f"'{value}'"
|
|
90
|
+
if self.output_format == OutputFormat.DETAILED_TEXT:
|
|
91
|
+
tmp = f"{tmp} (name='{choice_name}')"
|
|
92
|
+
return tmp
|
|
93
|
+
|
|
94
|
+
else:
|
|
95
|
+
message = (
|
|
96
|
+
f"選択肢IDが'{value}'である選択肢は存在しません。 :: "
|
|
97
|
+
f"属性名='{AnnofabApiFacade.get_additional_data_definition_name_en(attribute)}', "
|
|
98
|
+
f"属性ID='{attribute['additional_data_definition_id']}'"
|
|
99
|
+
)
|
|
100
|
+
logger.warning(message)
|
|
101
|
+
if self.raise_if_not_found:
|
|
102
|
+
raise ValueError(message)
|
|
103
|
+
return f"'{value}'"
|
|
104
|
+
else:
|
|
105
|
+
return f"'{value}'"
|
|
106
|
+
|
|
107
|
+
def get_restriction_text(self, attribute_id: str, condition: dict[str, Any]) -> str: # noqa: PLR0912
|
|
108
|
+
"""制約情報のテキストを返します。
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
attribute_id (str): 属性ID
|
|
112
|
+
condition (dict[str, Any]): 制約条件
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
str: 制約を表す文
|
|
116
|
+
"""
|
|
117
|
+
str_type = condition["_type"]
|
|
118
|
+
|
|
119
|
+
if str_type == "Imply":
|
|
120
|
+
# 属性間の制約
|
|
121
|
+
premise = condition["premise"]
|
|
122
|
+
if_condition_text = self.get_restriction_text(premise["additional_data_definition_id"], premise["condition"])
|
|
123
|
+
then_text = self.get_restriction_text(attribute_id, condition["condition"])
|
|
124
|
+
return f"{then_text} IF {if_condition_text}"
|
|
125
|
+
|
|
126
|
+
attribute = self.attribute_dict.get(attribute_id)
|
|
127
|
+
if attribute is not None:
|
|
128
|
+
subject = f"'{AnnofabApiFacade.get_additional_data_definition_name_en(attribute)}'"
|
|
129
|
+
if self.output_format == OutputFormat.DETAILED_TEXT:
|
|
130
|
+
subject = f"{subject} (id='{attribute_id}', type='{attribute['type']}')"
|
|
131
|
+
else:
|
|
132
|
+
logger.warning(f"属性IDが'{attribute_id}'である属性は存在しません。")
|
|
133
|
+
if self.raise_if_not_found:
|
|
134
|
+
raise ValueError(f"属性IDが'{attribute_id}'である属性は存在しません。")
|
|
135
|
+
|
|
136
|
+
subject = "''"
|
|
137
|
+
if self.output_format == OutputFormat.DETAILED_TEXT:
|
|
138
|
+
subject = f"{subject} (id='{attribute_id}')"
|
|
139
|
+
|
|
140
|
+
if str_type == "CanInput":
|
|
141
|
+
verb = "CAN INPUT" if condition["enable"] else "CAN NOT INPUT"
|
|
142
|
+
str_object = ""
|
|
143
|
+
|
|
144
|
+
elif str_type == "HasLabel":
|
|
145
|
+
verb = "HAS LABEL"
|
|
146
|
+
str_object = self.get_labels_text(condition["labels"])
|
|
147
|
+
|
|
148
|
+
elif str_type == "Equals":
|
|
149
|
+
verb = "EQUALS"
|
|
150
|
+
str_object = self.get_object_for_equals_or_notequals(condition["value"], attribute)
|
|
151
|
+
|
|
152
|
+
elif str_type == "NotEquals":
|
|
153
|
+
verb = "DOES NOT EQUAL"
|
|
154
|
+
str_object = self.get_object_for_equals_or_notequals(condition["value"], attribute)
|
|
155
|
+
|
|
156
|
+
elif str_type == "Matches":
|
|
157
|
+
verb = "MATCHES"
|
|
158
|
+
str_object = f"'{condition['value']}'"
|
|
159
|
+
|
|
160
|
+
elif str_type == "NotMatches":
|
|
161
|
+
verb = "DOES NOT MATCH"
|
|
162
|
+
str_object = f"'{condition['value']}'"
|
|
163
|
+
else:
|
|
164
|
+
raise ValueError(f"condition._type='{str_type}'はサポートしていません。")
|
|
165
|
+
|
|
166
|
+
tmp = f"{subject} {verb}"
|
|
167
|
+
if str_object != "":
|
|
168
|
+
tmp = f"{tmp} {str_object}"
|
|
169
|
+
return tmp
|
|
170
|
+
|
|
171
|
+
def get_attribute_from_name(self, attribute_name: str) -> Optional[dict[str, Any]]:
|
|
172
|
+
tmp = [
|
|
173
|
+
attribute
|
|
174
|
+
for attribute in self.attribute_dict.values()
|
|
175
|
+
if AnnofabApiFacade.get_additional_data_definition_name_en(attribute) == attribute_name
|
|
176
|
+
]
|
|
177
|
+
if len(tmp) == 1:
|
|
178
|
+
return tmp[0]
|
|
179
|
+
elif len(tmp) == 0:
|
|
180
|
+
logger.warning(f"属性名(英語)が'{attribute_name}'の属性は存在しません。")
|
|
181
|
+
return None
|
|
182
|
+
else:
|
|
183
|
+
logger.warning(f"属性名(英語)が'{attribute_name}'の属性は複数存在します。")
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
def get_label_from_name(self, label_name: str) -> Optional[dict[str, Any]]:
|
|
187
|
+
tmp = [label for label in self.label_dict.values() if AnnofabApiFacade.get_label_name_en(label) == label_name]
|
|
188
|
+
if len(tmp) == 1:
|
|
189
|
+
return tmp[0]
|
|
190
|
+
elif len(tmp) == 0:
|
|
191
|
+
logger.warning(f"ラベル名(英語)が'{label_name}'のラベルは存在しません。")
|
|
192
|
+
return None
|
|
193
|
+
else:
|
|
194
|
+
logger.warning(f"ラベル名(英語)が'{label_name}'のラベルは複数存在します。")
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
def get_target_attribute_ids(
|
|
198
|
+
self,
|
|
199
|
+
target_attribute_names: Optional[Collection[str]] = None,
|
|
200
|
+
target_label_names: Optional[Collection[str]] = None,
|
|
201
|
+
) -> set[str]:
|
|
202
|
+
result: set[str] = set()
|
|
203
|
+
|
|
204
|
+
if target_attribute_names is not None:
|
|
205
|
+
tmp_attribute_list = [self.get_attribute_from_name(attribute_name) for attribute_name in target_attribute_names]
|
|
206
|
+
tmp_ids = {attribute["additional_data_definition_id"] for attribute in tmp_attribute_list if attribute is not None}
|
|
207
|
+
result = result | tmp_ids
|
|
208
|
+
|
|
209
|
+
if target_label_names is not None:
|
|
210
|
+
tmp_label_list = [self.get_label_from_name(label_name) for label_name in target_label_names]
|
|
211
|
+
tmp_ids_list = [label["additional_data_definitions"] for label in tmp_label_list if label is not None]
|
|
212
|
+
for attribute_ids in tmp_ids_list:
|
|
213
|
+
result = result | set(attribute_ids)
|
|
214
|
+
|
|
215
|
+
return result
|
|
216
|
+
|
|
217
|
+
def get_restriction_text_list(
|
|
218
|
+
self,
|
|
219
|
+
restrictions: list[dict[str, Any]],
|
|
220
|
+
*,
|
|
221
|
+
target_attribute_names: Optional[Collection[str]] = None,
|
|
222
|
+
target_label_names: Optional[Collection[str]] = None,
|
|
223
|
+
) -> list[str]:
|
|
224
|
+
"""
|
|
225
|
+
複数の属性制約から自然言語で記載されたメッセージのlistを返します。
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
restrictions: 属性制約のリスト
|
|
229
|
+
target_attribute_names: 取得対象のラベル名(英語)のlist
|
|
230
|
+
target_label_names: 取得対象の属性名(英語)のlist
|
|
231
|
+
"""
|
|
232
|
+
if target_attribute_names is not None or target_label_names is not None:
|
|
233
|
+
target_attribute_ids = self.get_target_attribute_ids(target_attribute_names=target_attribute_names, target_label_names=target_label_names)
|
|
234
|
+
return [
|
|
235
|
+
self.get_restriction_text(e["additional_data_definition_id"], e["condition"])
|
|
236
|
+
for e in restrictions
|
|
237
|
+
if e["additional_data_definition_id"] in target_attribute_ids
|
|
238
|
+
]
|
|
239
|
+
else:
|
|
240
|
+
return [self.get_restriction_text(e["additional_data_definition_id"], e["condition"]) for e in restrictions]
|
|
@@ -4,15 +4,12 @@ import argparse
|
|
|
4
4
|
import json
|
|
5
5
|
import logging
|
|
6
6
|
import sys
|
|
7
|
-
from collections.abc import Collection
|
|
8
|
-
from enum import Enum
|
|
9
7
|
from pathlib import Path
|
|
10
|
-
from typing import
|
|
11
|
-
|
|
12
|
-
from more_itertools import first_true
|
|
8
|
+
from typing import Optional
|
|
13
9
|
|
|
14
10
|
import annofabcli
|
|
15
11
|
import annofabcli.common.cli
|
|
12
|
+
from annofabcli.annotation_specs.attribute_restriction import AttributeRestrictionMessage, OutputFormat
|
|
16
13
|
from annofabcli.common.cli import (
|
|
17
14
|
COMMAND_LINE_ERROR_STATUS_CODE,
|
|
18
15
|
ArgumentParser,
|
|
@@ -25,204 +22,6 @@ from annofabcli.common.facade import AnnofabApiFacade
|
|
|
25
22
|
logger = logging.getLogger(__name__)
|
|
26
23
|
|
|
27
24
|
|
|
28
|
-
class FormatArgument(Enum):
|
|
29
|
-
"""
|
|
30
|
-
表示するフォーマット ``--format`` で指定できる値
|
|
31
|
-
|
|
32
|
-
Attributes:
|
|
33
|
-
TEXT: 属性IDや種類を隠したシンプルなテキスト
|
|
34
|
-
DETAILED_TEXT: 属性IDや属性種類などの詳細情報を表示したテキスト
|
|
35
|
-
"""
|
|
36
|
-
|
|
37
|
-
TEXT = "text"
|
|
38
|
-
DETAILED_TEXT = "detailed_text"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class ListAttributeRestrictionMain:
|
|
42
|
-
"""
|
|
43
|
-
アノテーション仕様の制約から自然言語で書かれたメッセージを生成します。
|
|
44
|
-
|
|
45
|
-
Args:
|
|
46
|
-
labels: アノテーション仕様のラベル情報
|
|
47
|
-
additionals: アノテーション仕様の属性情報
|
|
48
|
-
|
|
49
|
-
"""
|
|
50
|
-
|
|
51
|
-
def __init__(
|
|
52
|
-
self,
|
|
53
|
-
labels: list[dict[str, Any]],
|
|
54
|
-
additionals: list[dict[str, Any]],
|
|
55
|
-
format: FormatArgument = FormatArgument.DETAILED_TEXT, # noqa: A002 # pylint: disable=redefined-builtin
|
|
56
|
-
) -> None:
|
|
57
|
-
self.attribute_dict = {e["additional_data_definition_id"]: e for e in additionals}
|
|
58
|
-
self.label_dict = {e["label_id"]: e for e in labels}
|
|
59
|
-
self.format = format
|
|
60
|
-
|
|
61
|
-
def get_labels_text(self, label_ids: Collection[str]) -> str:
|
|
62
|
-
label_message_list = []
|
|
63
|
-
for label_id in label_ids:
|
|
64
|
-
label = self.label_dict.get(label_id)
|
|
65
|
-
label_name = AnnofabApiFacade.get_label_name_en(label) if label is not None else ""
|
|
66
|
-
|
|
67
|
-
label_message = f"'{label_name}'"
|
|
68
|
-
if self.format == FormatArgument.DETAILED_TEXT:
|
|
69
|
-
label_message = f"{label_message} (id='{label_id}')"
|
|
70
|
-
label_message_list.append(label_message)
|
|
71
|
-
|
|
72
|
-
return ", ".join(label_message_list)
|
|
73
|
-
|
|
74
|
-
def get_object_for_equals_or_notequals(self, value: str, attribute: Optional[dict[str, Any]]) -> str:
|
|
75
|
-
"""制約条件が `Equals` or `NotEquals`のときの目的語を生成する。
|
|
76
|
-
属性の種類がドロップダウンかセレクトボックスのときは、選択肢の名前を返す。
|
|
77
|
-
|
|
78
|
-
Args:
|
|
79
|
-
value (str): _description_
|
|
80
|
-
attribute (Optional[dict[str,Any]]): _description_
|
|
81
|
-
|
|
82
|
-
Returns:
|
|
83
|
-
str: _description_
|
|
84
|
-
"""
|
|
85
|
-
if attribute is not None and attribute["type"] in ["choice", "select"]:
|
|
86
|
-
# ラジオボタンかドロップダウンのとき
|
|
87
|
-
choices = attribute["choices"]
|
|
88
|
-
choice = first_true(choices, pred=lambda e: e["choice_id"] == value)
|
|
89
|
-
if choice is not None:
|
|
90
|
-
choice_name = AnnofabApiFacade.get_choice_name_en(choice)
|
|
91
|
-
tmp = f"'{value}'"
|
|
92
|
-
if self.format == FormatArgument.DETAILED_TEXT:
|
|
93
|
-
tmp = f"{tmp} (name='{choice_name}')"
|
|
94
|
-
return tmp
|
|
95
|
-
|
|
96
|
-
else:
|
|
97
|
-
return f"'{value}'"
|
|
98
|
-
else:
|
|
99
|
-
return f"'{value}'"
|
|
100
|
-
|
|
101
|
-
def get_restriction_text(self, attribute_id: str, condition: dict[str, Any]) -> str: # noqa: PLR0912
|
|
102
|
-
"""制約情報のテキストを返します。
|
|
103
|
-
|
|
104
|
-
Args:
|
|
105
|
-
attribute_id (str): 属性ID
|
|
106
|
-
condition (dict[str, Any]): 制約条件
|
|
107
|
-
|
|
108
|
-
Returns:
|
|
109
|
-
str: 制約を表す文
|
|
110
|
-
"""
|
|
111
|
-
str_type = condition["_type"]
|
|
112
|
-
|
|
113
|
-
if str_type == "Imply":
|
|
114
|
-
# 属性間の制約
|
|
115
|
-
premise = condition["premise"]
|
|
116
|
-
if_condition_text = self.get_restriction_text(premise["additional_data_definition_id"], premise["condition"])
|
|
117
|
-
then_text = self.get_restriction_text(attribute_id, condition["condition"])
|
|
118
|
-
return f"{then_text} IF {if_condition_text}"
|
|
119
|
-
|
|
120
|
-
attribute = self.attribute_dict.get(attribute_id)
|
|
121
|
-
if attribute is not None:
|
|
122
|
-
subject = f"'{AnnofabApiFacade.get_additional_data_definition_name_en(attribute)}'"
|
|
123
|
-
if self.format == FormatArgument.DETAILED_TEXT:
|
|
124
|
-
subject = f"{subject} (id='{attribute_id}', type='{attribute['type']}')"
|
|
125
|
-
else:
|
|
126
|
-
logger.warning(f"属性IDが'{attribute_id}'の属性は存在しません。")
|
|
127
|
-
subject = "''"
|
|
128
|
-
if self.format == FormatArgument.DETAILED_TEXT:
|
|
129
|
-
subject = f"{subject} (id='{attribute_id}')"
|
|
130
|
-
|
|
131
|
-
if str_type == "CanInput":
|
|
132
|
-
verb = "CAN INPUT" if condition["enable"] else "CAN NOT INPUT"
|
|
133
|
-
str_object = ""
|
|
134
|
-
|
|
135
|
-
elif str_type == "HasLabel":
|
|
136
|
-
verb = "HAS LABEL"
|
|
137
|
-
str_object = self.get_labels_text(condition["labels"])
|
|
138
|
-
|
|
139
|
-
elif str_type == "Equals":
|
|
140
|
-
verb = "EQUALS"
|
|
141
|
-
str_object = self.get_object_for_equals_or_notequals(condition["value"], attribute)
|
|
142
|
-
|
|
143
|
-
elif str_type == "NotEquals":
|
|
144
|
-
verb = "DOES NOT EQUAL"
|
|
145
|
-
str_object = self.get_object_for_equals_or_notequals(condition["value"], attribute)
|
|
146
|
-
|
|
147
|
-
elif str_type == "Matches":
|
|
148
|
-
verb = "MATCHES"
|
|
149
|
-
str_object = f"'{condition['value']}'"
|
|
150
|
-
|
|
151
|
-
elif str_type == "NotMatches":
|
|
152
|
-
verb = "DOES NOT MATCH"
|
|
153
|
-
str_object = f"'{condition['value']}'"
|
|
154
|
-
else:
|
|
155
|
-
raise RuntimeError(f"{str_type=}はサポートしていません。")
|
|
156
|
-
|
|
157
|
-
tmp = f"{subject} {verb}"
|
|
158
|
-
if str_object != "":
|
|
159
|
-
tmp = f"{tmp} {str_object}"
|
|
160
|
-
return tmp
|
|
161
|
-
|
|
162
|
-
def get_attribute_from_name(self, attribute_name: str) -> Optional[dict[str, Any]]:
|
|
163
|
-
tmp = [
|
|
164
|
-
attribute
|
|
165
|
-
for attribute in self.attribute_dict.values()
|
|
166
|
-
if AnnofabApiFacade.get_additional_data_definition_name_en(attribute) == attribute_name
|
|
167
|
-
]
|
|
168
|
-
if len(tmp) == 1:
|
|
169
|
-
return tmp[0]
|
|
170
|
-
elif len(tmp) == 0:
|
|
171
|
-
logger.warning(f"属性名(英語)が'{attribute_name}'の属性は存在しません。")
|
|
172
|
-
return None
|
|
173
|
-
else:
|
|
174
|
-
logger.warning(f"属性名(英語)が'{attribute_name}'の属性は複数存在します。")
|
|
175
|
-
return None
|
|
176
|
-
|
|
177
|
-
def get_label_from_name(self, label_name: str) -> Optional[dict[str, Any]]:
|
|
178
|
-
tmp = [label for label in self.label_dict.values() if AnnofabApiFacade.get_label_name_en(label) == label_name]
|
|
179
|
-
if len(tmp) == 1:
|
|
180
|
-
return tmp[0]
|
|
181
|
-
elif len(tmp) == 0:
|
|
182
|
-
logger.warning(f"ラベル名(英語)が'{label_name}'のラベルは存在しません。")
|
|
183
|
-
return None
|
|
184
|
-
else:
|
|
185
|
-
logger.warning(f"ラベル名(英語)が'{label_name}'のラベルは複数存在します。")
|
|
186
|
-
return None
|
|
187
|
-
|
|
188
|
-
def get_target_attribute_ids(
|
|
189
|
-
self,
|
|
190
|
-
target_attribute_names: Optional[Collection[str]] = None,
|
|
191
|
-
target_label_names: Optional[Collection[str]] = None,
|
|
192
|
-
) -> set[str]:
|
|
193
|
-
result: set[str] = set()
|
|
194
|
-
|
|
195
|
-
if target_attribute_names is not None:
|
|
196
|
-
tmp_attribute_list = [self.get_attribute_from_name(attribute_name) for attribute_name in target_attribute_names]
|
|
197
|
-
tmp_ids = {attribute["additional_data_definition_id"] for attribute in tmp_attribute_list if attribute is not None}
|
|
198
|
-
result = result | tmp_ids
|
|
199
|
-
|
|
200
|
-
if target_label_names is not None:
|
|
201
|
-
tmp_label_list = [self.get_label_from_name(label_name) for label_name in target_label_names]
|
|
202
|
-
tmp_ids_list = [label["additional_data_definitions"] for label in tmp_label_list if label is not None]
|
|
203
|
-
for attribute_ids in tmp_ids_list:
|
|
204
|
-
result = result | set(attribute_ids)
|
|
205
|
-
|
|
206
|
-
return result
|
|
207
|
-
|
|
208
|
-
def get_restriction_text_list(
|
|
209
|
-
self,
|
|
210
|
-
restrictions: list[dict[str, Any]],
|
|
211
|
-
*,
|
|
212
|
-
target_attribute_names: Optional[Collection[str]] = None,
|
|
213
|
-
target_label_names: Optional[Collection[str]] = None,
|
|
214
|
-
) -> list[str]:
|
|
215
|
-
if target_attribute_names is not None or target_label_names is not None:
|
|
216
|
-
target_attribute_ids = self.get_target_attribute_ids(target_attribute_names=target_attribute_names, target_label_names=target_label_names)
|
|
217
|
-
return [
|
|
218
|
-
self.get_restriction_text(e["additional_data_definition_id"], e["condition"])
|
|
219
|
-
for e in restrictions
|
|
220
|
-
if e["additional_data_definition_id"] in target_attribute_ids
|
|
221
|
-
]
|
|
222
|
-
else:
|
|
223
|
-
return [self.get_restriction_text(e["additional_data_definition_id"], e["condition"]) for e in restrictions]
|
|
224
|
-
|
|
225
|
-
|
|
226
25
|
class ListAttributeRestriction(CommandLine):
|
|
227
26
|
COMMON_MESSAGE = "annofabcli annotation_specs list_restriction: error:"
|
|
228
27
|
|
|
@@ -263,10 +62,10 @@ class ListAttributeRestriction(CommandLine):
|
|
|
263
62
|
else:
|
|
264
63
|
raise RuntimeError("'--project_id'か'--annotation_specs_json'のどちらかを指定する必要があります。")
|
|
265
64
|
|
|
266
|
-
main_obj =
|
|
65
|
+
main_obj = AttributeRestrictionMessage(
|
|
267
66
|
labels=annotation_specs["labels"],
|
|
268
67
|
additionals=annotation_specs["additionals"],
|
|
269
|
-
|
|
68
|
+
output_format=OutputFormat(args.format),
|
|
270
69
|
)
|
|
271
70
|
target_attribute_names = get_list_from_args(args.attribute_name) if args.attribute_name is not None else None
|
|
272
71
|
target_label_names = get_list_from_args(args.label_name) if args.label_name is not None else None
|
|
@@ -323,12 +122,12 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
323
122
|
"-f",
|
|
324
123
|
"--format",
|
|
325
124
|
type=str,
|
|
326
|
-
choices=[e.value for e in
|
|
327
|
-
default=
|
|
125
|
+
choices=[e.value for e in OutputFormat],
|
|
126
|
+
default=OutputFormat.TEXT.value,
|
|
328
127
|
help=f"出力フォーマット\n"
|
|
329
128
|
"\n"
|
|
330
|
-
f"* {
|
|
331
|
-
f"* {
|
|
129
|
+
f"* {OutputFormat.TEXT.value}: 英語名のみ出力する形式\n"
|
|
130
|
+
f"* {OutputFormat.DETAILED_TEXT.value}: 属性IDや属性種類などの詳細情報を出力する形式\n",
|
|
332
131
|
)
|
|
333
132
|
|
|
334
133
|
parser.set_defaults(subcommand_func=main)
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import logging
|
|
5
|
+
import sys
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from typing import Any, Optional
|
|
7
8
|
|
|
@@ -10,6 +11,7 @@ import annofabapi
|
|
|
10
11
|
import annofabcli
|
|
11
12
|
import annofabcli.common.cli
|
|
12
13
|
from annofabcli.common.cli import (
|
|
14
|
+
COMMAND_LINE_ERROR_STATUS_CODE,
|
|
13
15
|
ArgumentParser,
|
|
14
16
|
CommandLine,
|
|
15
17
|
CommandLineWithConfirm,
|
|
@@ -100,6 +102,9 @@ class PutLabelColor(CommandLine):
|
|
|
100
102
|
args = self.args
|
|
101
103
|
label_color = get_json_from_args(args.json)
|
|
102
104
|
|
|
105
|
+
if not isinstance(label_color, dict):
|
|
106
|
+
print("annofabcli annotation_specs put_label_color: error: JSON形式が不正です。オブジェクトを指定してください。", file=sys.stderr) # noqa: T201
|
|
107
|
+
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
103
108
|
main_obj = PuttingLabelColorMain(service=self.service, project_id=args.project_id, all_yes=args.yes)
|
|
104
109
|
main_obj.main(label_color, comment=args.comment)
|
|
105
110
|
|