annofabcli 1.99.0__py3-none-any.whl → 100.2__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 (31) hide show
  1. annofabcli/__version__.py +1 -1
  2. annofabcli/annotation/change_annotation_attributes.py +2 -2
  3. annofabcli/annotation_specs/add_attribute_restriction.py +158 -0
  4. annofabcli/annotation_specs/attribute_restriction.py +240 -0
  5. annofabcli/annotation_specs/list_annotation_specs_attribute.py +51 -33
  6. annofabcli/annotation_specs/list_annotation_specs_choice.py +42 -17
  7. annofabcli/annotation_specs/list_annotation_specs_label.py +26 -93
  8. annofabcli/annotation_specs/list_annotation_specs_label_attribute.py +220 -0
  9. annofabcli/annotation_specs/list_attribute_restriction.py +8 -209
  10. annofabcli/annotation_specs/subcommand_annotation_specs.py +4 -0
  11. annofabcli/common/annofab/annotation_specs.py +25 -0
  12. annofabcli/common/download.py +1 -1
  13. annofabcli/common/exceptions.py +2 -2
  14. annofabcli/common/facade.py +2 -2
  15. annofabcli/common/image.py +2 -2
  16. annofabcli/input_data/delete_input_data.py +6 -7
  17. annofabcli/job/delete_job.py +4 -4
  18. annofabcli/organization_member/list_organization_member.py +2 -3
  19. annofabcli/project_member/change_project_members.py +2 -3
  20. annofabcli/project_member/drop_project_members.py +8 -8
  21. annofabcli/project_member/invite_project_members.py +5 -6
  22. annofabcli/project_member/list_users.py +4 -4
  23. annofabcli/task/complete_tasks.py +2 -2
  24. annofabcli/task/reject_tasks.py +76 -100
  25. annofabcli/task/update_metadata_of_task.py +1 -1
  26. annofabcli/task_history/list_task_history.py +1 -1
  27. {annofabcli-1.99.0.dist-info → annofabcli-100.2.dist-info}/METADATA +1 -1
  28. {annofabcli-1.99.0.dist-info → annofabcli-100.2.dist-info}/RECORD +31 -27
  29. {annofabcli-1.99.0.dist-info → annofabcli-100.2.dist-info}/LICENSE +0 -0
  30. {annofabcli-1.99.0.dist-info → annofabcli-100.2.dist-info}/WHEEL +0 -0
  31. {annofabcli-1.99.0.dist-info → annofabcli-100.2.dist-info}/entry_points.txt +0 -0
annofabcli/__version__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.99.0" # `poetry-dynamic-versioning`を使ってGitHubのバージョンタグを取得している。変更不要
1
+ __version__ = "100.2" # `poetry-dynamic-versioning`を使ってGitHubのバージョンタグを取得している。変更不要
@@ -138,12 +138,12 @@ class ChangeAnnotationAttributesMain(CommandLineWithConfirm):
138
138
 
139
139
  task: Task = Task.from_dict(dict_task)
140
140
  if task.status == TaskStatus.WORKING:
141
- logger.warning(f"task_id={task_id}: タスクが作業中状態のため、スキップします。")
141
+ logger.warning(f"task_id='{task_id}': タスクが作業中状態のため、スキップします。")
142
142
  return False
143
143
 
144
144
  if not self.is_force: # noqa: SIM102
145
145
  if task.status == TaskStatus.COMPLETE:
146
- logger.warning(f"task_id={task_id}: タスクが完了状態のため、スキップします。")
146
+ logger.warning(f"task_id='{task_id}': タスクが完了状態のため、スキップします。")
147
147
  return False
148
148
 
149
149
  annotation_list = self.get_annotation_list_for_task(task_id, annotation_query)
@@ -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]
@@ -16,6 +16,7 @@ from dataclasses_json import DataClassJsonMixin
16
16
 
17
17
  import annofabcli
18
18
  import annofabcli.common.cli
19
+ from annofabcli.common.annofab.annotation_specs import keybind_to_text
19
20
  from annofabcli.common.cli import (
20
21
  COMMAND_LINE_ERROR_STATUS_CODE,
21
22
  ArgumentParser,
@@ -24,15 +25,16 @@ from annofabcli.common.cli import (
24
25
  )
25
26
  from annofabcli.common.enums import FormatArgument
26
27
  from annofabcli.common.facade import AnnofabApiFacade
27
- from annofabcli.common.utils import print_csv
28
+ from annofabcli.common.utils import print_according_to_format, print_csv
28
29
 
29
30
  logger = logging.getLogger(__name__)
30
31
 
31
32
 
32
33
  @dataclass
33
- class AttributeForCsv(DataClassJsonMixin):
34
+ class FlattenAttribute(DataClassJsonMixin):
34
35
  """
35
- CSV用の属性情報を格納するクラスです。
36
+ ネストされていないフラットな属性情報を格納するクラスです。
37
+ 選択肢の一覧などは格納できません。
36
38
  """
37
39
 
38
40
  attribute_id: str
@@ -45,7 +47,7 @@ class AttributeForCsv(DataClassJsonMixin):
45
47
  attribute_name_en: Optional[str]
46
48
  attribute_name_ja: Optional[str]
47
49
  attribute_name_vi: Optional[str]
48
- type: str
50
+ attribute_type: str
49
51
  """属性の種類"""
50
52
  default: Optional[Union[str, bool, int]]
51
53
  """デフォルト値"""
@@ -60,6 +62,7 @@ class AttributeForCsv(DataClassJsonMixin):
60
62
  """制約の個数"""
61
63
  reference_label_count: int
62
64
  """参照されているラベルの個数"""
65
+ keybind: Optional[str]
63
66
 
64
67
 
65
68
  def create_relationship_between_attribute_and_label(labels_v3: list[dict[str, Any]]) -> dict[str, set[str]]:
@@ -78,11 +81,11 @@ def create_relationship_between_attribute_and_label(labels_v3: list[dict[str, An
78
81
  return result
79
82
 
80
83
 
81
- def create_df_from_additionals(
84
+ def create_flatten_attribute_list_from_additionals(
82
85
  additionals_v3: list[dict[str, Any]], labels_v3: list[dict[str, Any]], restrictions: list[dict[str, Any]]
83
- ) -> pandas.DataFrame:
86
+ ) -> list[FlattenAttribute]:
84
87
  """
85
- APIから取得した属性情報(v3版)から、pandas.DataFrameを生成します。
88
+ APIから取得した属性情報(v3版)から、属性情報の一覧を生成します。
86
89
 
87
90
  Args:
88
91
  additionals_v3: APIから取得した属性情報(v3版)
@@ -98,52 +101,60 @@ def create_df_from_additionals(
98
101
  # keyが属性ID、valueが属性に紐づくラベルのIDのsetであるdict
99
102
  dict_label_ids = create_relationship_between_attribute_and_label(labels_v3)
100
103
 
101
- def dict_additional_to_dataclass(additional: dict[str, Any]) -> AttributeForCsv:
104
+ def dict_additional_to_dataclass(additional: dict[str, Any]) -> FlattenAttribute:
102
105
  """
103
106
  辞書の属性情報をDataClassのラベル情報に変換します。
104
107
  """
105
108
  attribute_id = additional["additional_data_definition_id"]
106
109
  additional_name = additional["name"]
107
- return AttributeForCsv(
110
+ return FlattenAttribute(
108
111
  attribute_id=attribute_id,
109
112
  attribute_name_en=get_message_with_lang(additional_name, lang=Lang.EN_US),
110
113
  attribute_name_ja=get_message_with_lang(additional_name, lang=Lang.JA_JP),
111
114
  attribute_name_vi=get_message_with_lang(additional_name, lang=Lang.VI_VN),
112
- type=additional["type"],
115
+ attribute_type=additional["type"],
113
116
  default=additional["default"],
114
117
  read_only=additional["read_only"],
115
118
  choice_count=len(additional["choices"]),
116
119
  restriction_count=dict_restriction_count[attribute_id],
117
120
  reference_label_count=len(dict_label_ids[attribute_id]),
121
+ keybind=keybind_to_text(additional["keybind"]),
118
122
  )
119
123
 
120
- new_labels = [dict_additional_to_dataclass(e) for e in additionals_v3]
121
-
122
- columns = [
123
- "attribute_id",
124
- "attribute_name_en",
125
- "attribute_name_ja",
126
- "attribute_name_vi",
127
- "type",
128
- "default",
129
- "read_only",
130
- "choice_count",
131
- "restriction_count",
132
- "reference_label_count",
133
- ]
134
-
135
- df = pandas.DataFrame(new_labels, columns=columns)
136
- return df
124
+ return [dict_additional_to_dataclass(e) for e in additionals_v3]
137
125
 
138
126
 
139
127
  class PrintAnnotationSpecsAttribute(CommandLine):
140
128
  COMMON_MESSAGE = "annofabcli annotation_specs list_attribute: error:"
141
129
 
142
- def print_annotation_specs_attribute(self, annotation_specs_v3: dict[str, Any], arg_format: str, output: Optional[str] = None) -> None:
143
- if arg_format == FormatArgument.CSV.value:
144
- df = create_df_from_additionals(annotation_specs_v3["additionals"], annotation_specs_v3["labels"], annotation_specs_v3["restrictions"])
130
+ def print_annotation_specs_attribute(
131
+ self, annotation_specs_v3: dict[str, Any], output_format: FormatArgument, output: Optional[str] = None
132
+ ) -> None:
133
+ attribute_list = create_flatten_attribute_list_from_additionals(
134
+ annotation_specs_v3["additionals"], annotation_specs_v3["labels"], annotation_specs_v3["restrictions"]
135
+ )
136
+ logger.info(f"{len(attribute_list)} 件の属性情報を出力します。")
137
+ if output_format == FormatArgument.CSV:
138
+ columns = [
139
+ "attribute_id",
140
+ "attribute_name_en",
141
+ "attribute_name_ja",
142
+ "attribute_name_vi",
143
+ "type",
144
+ "default",
145
+ "read_only",
146
+ "choice_count",
147
+ "restriction_count",
148
+ "reference_label_count",
149
+ "keybind",
150
+ ]
151
+
152
+ df = pandas.DataFrame(attribute_list, columns=columns)
145
153
  print_csv(df, output)
146
154
 
155
+ elif output_format in [FormatArgument.JSON, FormatArgument.PRETTY_JSON]:
156
+ print_according_to_format([e.to_dict() for e in attribute_list], format=output_format, output=output)
157
+
147
158
  def get_history_id_from_before_index(self, project_id: str, before: int) -> Optional[str]:
148
159
  histories, _ = self.service.api.get_annotation_specs_histories(project_id)
149
160
  if before + 1 > len(histories):
@@ -177,7 +188,7 @@ class PrintAnnotationSpecsAttribute(CommandLine):
177
188
  else:
178
189
  raise RuntimeError("'--project_id'か'--annotation_specs_json'のどちらかを指定する必要があります。")
179
190
 
180
- self.print_annotation_specs_attribute(annotation_specs, arg_format=args.format, output=args.output)
191
+ self.print_annotation_specs_attribute(annotation_specs, output_format=FormatArgument(args.format), output=args.output)
181
192
 
182
193
 
183
194
  def parse_args(parser: argparse.ArgumentParser) -> None:
@@ -216,7 +227,14 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
216
227
  ),
217
228
  )
218
229
 
219
- parser.add_argument("-f", "--format", type=str, choices=[FormatArgument.CSV.value], default=FormatArgument.CSV.value, help="出力フォーマット ")
230
+ parser.add_argument(
231
+ "-f",
232
+ "--format",
233
+ type=str,
234
+ choices=[FormatArgument.CSV.value, FormatArgument.JSON.value, FormatArgument.PRETTY_JSON.value],
235
+ default=FormatArgument.CSV.value,
236
+ help="出力フォーマット ",
237
+ )
220
238
 
221
239
  argument_parser.add_output()
222
240
 
@@ -232,7 +250,7 @@ def main(args: argparse.Namespace) -> None:
232
250
  def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
233
251
  subcommand_name = "list_attribute"
234
252
 
235
- subcommand_help = "アノテーション仕様の属性情報を出力する"
253
+ subcommand_help = "アノテーション仕様の属性情報を出力します。"
236
254
 
237
255
  parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help)
238
256
  parse_args(parser)