From e34d095cf46f3c855e3301fb58a9e2ba21d9ece4 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Sun, 10 May 2026 00:38:04 +0900 Subject: [PATCH 1/9] Add project member repository --- annofabapi/project_member_repository.py | 93 +++++++++++++++++++++++++ tests/test_project_member_repository.py | 32 +++++++++ 2 files changed, 125 insertions(+) create mode 100644 annofabapi/project_member_repository.py create mode 100644 tests/test_project_member_repository.py diff --git a/annofabapi/project_member_repository.py b/annofabapi/project_member_repository.py new file mode 100644 index 00000000..9a105b8b --- /dev/null +++ b/annofabapi/project_member_repository.py @@ -0,0 +1,93 @@ +from collections.abc import Callable + +import more_itertools + +from annofabapi import Resource +from annofabapi.models import ProjectMember + + +class ProjectMemberRepository: + def __init__(self, resource: Resource) -> None: + self.resource = resource + self._members_by_project_id: dict[str, list[ProjectMember]] = {} + + def _get_project_member_with_predicate(self, project_id: str, predicate: Callable[[ProjectMember], bool]) -> ProjectMember | None: + """ + project_memberを取得する + + Args: + project_id: + predicate: 組織メンバの検索条件 + + Returns: + プロジェクトメンバ + """ + project_member_list = self._members_by_project_id.get(project_id) + if project_member_list is None: + project_member_list = self.resource.wrapper.get_all_project_members(project_id, query_params={"include_inactive_member": True}) + self._members_by_project_id[project_id] = project_member_list + return more_itertools.first_true(project_member_list, pred=predicate) + + def get_project_member_from_account_id(self, project_id: str, account_id: str) -> ProjectMember | None: + """ + account_idからプロジェクトメンバを取得する。 + + Args: + project_id: + account_id: + + Returns: + プロジェクトメンバ。見つからない場合はNone + """ + return self._get_project_member_with_predicate(project_id, predicate=lambda e: e["account_id"] == account_id) + + def get_project_member_from_user_id(self, project_id: str, user_id: str) -> ProjectMember | None: + """ + user_idからプロジェクトメンバを取得する。 + + Args: + project_id: + user_id: + + Returns: + プロジェクトメンバ。見つからない場合はNone + """ + return self._get_project_member_with_predicate(project_id, predicate=lambda e: e["user_id"] == user_id) + + def get_user_id_from_account_id(self, project_id: str, account_id: str) -> str: + """ + account_idからuser_idを取得する. + インスタンス変数に組織メンバがあれば、WebAPIは実行しない。 + + Args: + project_id: + account_id: + + Returns: + user_id + + Raises: + ValueError: 指定したaccount_idのプロジェクトメンバが見つからなかった場合 + """ + member = self.get_project_member_from_account_id(project_id, account_id) + if member is None: + raise ValueError(f"project_member is not found. project_id='{project_id}', account_id='{account_id}'") + return member["user_id"] + + def get_account_id_from_user_id(self, project_id: str, user_id: str) -> str | None: + """ + user_idからaccount_idを取得する。 + インスタンス変数に組織メンバがあれば、WebAPIは実行しない。 + + Args: + project_id: + user_id: + + Returns: + account_id. 見つからなければNone + + """ + member = self.get_project_member_from_user_id(project_id, user_id) + if member is None: + return None + return member["account_id"] diff --git a/tests/test_project_member_repository.py b/tests/test_project_member_repository.py new file mode 100644 index 00000000..82b46b7d --- /dev/null +++ b/tests/test_project_member_repository.py @@ -0,0 +1,32 @@ +import pytest + +from annofabapi.project_member_repository import ProjectMemberRepository + +PROJECT_ID = "project_id" + + +def create_repository_with_members(members): + repository = object.__new__(ProjectMemberRepository) + repository._members_by_project_id = {PROJECT_ID: members} + return repository + + +def test_get_user_id_from_account_id__存在するaccount_idならuser_idを返す(): + repository = create_repository_with_members( + [ + { + "project_id": PROJECT_ID, + "account_id": "account_id", + "user_id": "user_id", + } + ] + ) + + assert repository.get_user_id_from_account_id(PROJECT_ID, "account_id") == "user_id" + + +def test_get_user_id_from_account_id__存在しないaccount_idならValueErrorを送出する(): + repository = create_repository_with_members([]) + + with pytest.raises(ValueError): + repository.get_user_id_from_account_id(PROJECT_ID, "unknown_account_id") From 705d8234d5862d35d0e1d9af92f30d80fc828a00 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Sun, 10 May 2026 00:42:44 +0900 Subject: [PATCH 2/9] Raise error when project member user is missing --- annofabapi/project_member_repository.py | 17 +++++++++-------- tests/test_project_member_repository.py | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/annofabapi/project_member_repository.py b/annofabapi/project_member_repository.py index 9a105b8b..186cf9d5 100644 --- a/annofabapi/project_member_repository.py +++ b/annofabapi/project_member_repository.py @@ -57,11 +57,10 @@ def get_project_member_from_user_id(self, project_id: str, user_id: str) -> Proj def get_user_id_from_account_id(self, project_id: str, account_id: str) -> str: """ account_idからuser_idを取得する. - インスタンス変数に組織メンバがあれば、WebAPIは実行しない。 Args: - project_id: - account_id: + project_id: プロジェクトID + account_id: アカウントID Returns: user_id @@ -74,20 +73,22 @@ def get_user_id_from_account_id(self, project_id: str, account_id: str) -> str: raise ValueError(f"project_member is not found. project_id='{project_id}', account_id='{account_id}'") return member["user_id"] - def get_account_id_from_user_id(self, project_id: str, user_id: str) -> str | None: + def get_account_id_from_user_id(self, project_id: str, user_id: str) -> str: """ user_idからaccount_idを取得する。 インスタンス変数に組織メンバがあれば、WebAPIは実行しない。 Args: - project_id: - user_id: + project_id: プロジェクトID + user_id: ユーザーID Returns: - account_id. 見つからなければNone + account_id + Raises: + ValueError: 指定したuser_idのプロジェクトメンバが見つからなかった場合 """ member = self.get_project_member_from_user_id(project_id, user_id) if member is None: - return None + raise ValueError(f"project_member is not found. project_id='{project_id}', user_id='{user_id}'") return member["account_id"] diff --git a/tests/test_project_member_repository.py b/tests/test_project_member_repository.py index 82b46b7d..835f91ef 100644 --- a/tests/test_project_member_repository.py +++ b/tests/test_project_member_repository.py @@ -30,3 +30,24 @@ def test_get_user_id_from_account_id__存在しないaccount_idならValueError with pytest.raises(ValueError): repository.get_user_id_from_account_id(PROJECT_ID, "unknown_account_id") + + +def test_get_account_id_from_user_id__存在するuser_idならaccount_idを返す(): + repository = create_repository_with_members( + [ + { + "project_id": PROJECT_ID, + "account_id": "account_id", + "user_id": "user_id", + } + ] + ) + + assert repository.get_account_id_from_user_id(PROJECT_ID, "user_id") == "account_id" + + +def test_get_account_id_from_user_id__存在しないuser_idならValueErrorを送出する(): + repository = create_repository_with_members([]) + + with pytest.raises(ValueError): + repository.get_account_id_from_user_id(PROJECT_ID, "unknown_user_id") From 0417dc96fab8760666ca02c13dd0686596b57ece Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Sun, 10 May 2026 00:44:46 +0900 Subject: [PATCH 3/9] Revise project member repository docstrings --- annofabapi/project_member_repository.py | 46 ++++++++++++------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/annofabapi/project_member_repository.py b/annofabapi/project_member_repository.py index 186cf9d5..673eecf9 100644 --- a/annofabapi/project_member_repository.py +++ b/annofabapi/project_member_repository.py @@ -7,20 +7,23 @@ class ProjectMemberRepository: + """プロジェクトメンバ情報を取得するRepository。""" + def __init__(self, resource: Resource) -> None: self.resource = resource self._members_by_project_id: dict[str, list[ProjectMember]] = {} def _get_project_member_with_predicate(self, project_id: str, predicate: Callable[[ProjectMember], bool]) -> ProjectMember | None: - """ - project_memberを取得する + """条件に一致するプロジェクトメンバを取得する。 + + プロジェクトメンバの一覧をプロジェクトIDごとにキャッシュする。 Args: - project_id: - predicate: 組織メンバの検索条件 + project_id: プロジェクトID + predicate: プロジェクトメンバの検索条件 Returns: - プロジェクトメンバ + 条件に一致するプロジェクトメンバ。見つからない場合はNone。 """ project_member_list = self._members_by_project_id.get(project_id) if project_member_list is None: @@ -29,44 +32,41 @@ def _get_project_member_with_predicate(self, project_id: str, predicate: Callabl return more_itertools.first_true(project_member_list, pred=predicate) def get_project_member_from_account_id(self, project_id: str, account_id: str) -> ProjectMember | None: - """ - account_idからプロジェクトメンバを取得する。 + """account_idからプロジェクトメンバを取得する。 Args: - project_id: - account_id: + project_id: プロジェクトID + account_id: アカウントID Returns: - プロジェクトメンバ。見つからない場合はNone + 指定したaccount_idのプロジェクトメンバ。見つからない場合はNone。 """ return self._get_project_member_with_predicate(project_id, predicate=lambda e: e["account_id"] == account_id) def get_project_member_from_user_id(self, project_id: str, user_id: str) -> ProjectMember | None: - """ - user_idからプロジェクトメンバを取得する。 + """user_idからプロジェクトメンバを取得する。 Args: - project_id: - user_id: + project_id: プロジェクトID + user_id: ユーザーID Returns: - プロジェクトメンバ。見つからない場合はNone + 指定したuser_idのプロジェクトメンバ。見つからない場合はNone。 """ return self._get_project_member_with_predicate(project_id, predicate=lambda e: e["user_id"] == user_id) def get_user_id_from_account_id(self, project_id: str, account_id: str) -> str: - """ - account_idからuser_idを取得する. + """account_idからuser_idを取得する。 Args: project_id: プロジェクトID account_id: アカウントID Returns: - user_id + 指定したaccount_idに対応するユーザーID。 Raises: - ValueError: 指定したaccount_idのプロジェクトメンバが見つからなかった場合 + ValueError: 指定したaccount_idのプロジェクトメンバが見つからない場合。 """ member = self.get_project_member_from_account_id(project_id, account_id) if member is None: @@ -74,19 +74,17 @@ def get_user_id_from_account_id(self, project_id: str, account_id: str) -> str: return member["user_id"] def get_account_id_from_user_id(self, project_id: str, user_id: str) -> str: - """ - user_idからaccount_idを取得する。 - インスタンス変数に組織メンバがあれば、WebAPIは実行しない。 + """user_idからaccount_idを取得する。 Args: project_id: プロジェクトID user_id: ユーザーID Returns: - account_id + 指定したuser_idに対応するアカウントID。 Raises: - ValueError: 指定したuser_idのプロジェクトメンバが見つからなかった場合 + ValueError: 指定したuser_idのプロジェクトメンバが見つからない場合。 """ member = self.get_project_member_from_user_id(project_id, user_id) if member is None: From 02de3b7b47a9e146dd47de2bbd692893984d8141 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Sun, 10 May 2026 00:46:01 +0900 Subject: [PATCH 4/9] Raise error when project member is missing --- annofabapi/project_member_repository.py | 34 +++++++++++++---------- tests/test_project_member_repository.py | 36 +++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 14 deletions(-) diff --git a/annofabapi/project_member_repository.py b/annofabapi/project_member_repository.py index 673eecf9..f7832ed3 100644 --- a/annofabapi/project_member_repository.py +++ b/annofabapi/project_member_repository.py @@ -31,7 +31,7 @@ def _get_project_member_with_predicate(self, project_id: str, predicate: Callabl self._members_by_project_id[project_id] = project_member_list return more_itertools.first_true(project_member_list, pred=predicate) - def get_project_member_from_account_id(self, project_id: str, account_id: str) -> ProjectMember | None: + def get_project_member_from_account_id(self, project_id: str, account_id: str) -> ProjectMember: """account_idからプロジェクトメンバを取得する。 Args: @@ -39,11 +39,17 @@ def get_project_member_from_account_id(self, project_id: str, account_id: str) - account_id: アカウントID Returns: - 指定したaccount_idのプロジェクトメンバ。見つからない場合はNone。 + 指定したaccount_idのプロジェクトメンバ。 + + Raises: + ValueError: 指定したaccount_idのプロジェクトメンバが見つからない場合。 """ - return self._get_project_member_with_predicate(project_id, predicate=lambda e: e["account_id"] == account_id) + member = self._get_project_member_with_predicate(project_id, predicate=lambda e: e["account_id"] == account_id) + if member is None: + raise ValueError(f"project_member is not found. project_id='{project_id}', account_id='{account_id}'") + return member - def get_project_member_from_user_id(self, project_id: str, user_id: str) -> ProjectMember | None: + def get_project_member_from_user_id(self, project_id: str, user_id: str) -> ProjectMember: """user_idからプロジェクトメンバを取得する。 Args: @@ -51,9 +57,15 @@ def get_project_member_from_user_id(self, project_id: str, user_id: str) -> Proj user_id: ユーザーID Returns: - 指定したuser_idのプロジェクトメンバ。見つからない場合はNone。 + 指定したuser_idのプロジェクトメンバ。 + + Raises: + ValueError: 指定したuser_idのプロジェクトメンバが見つからない場合。 """ - return self._get_project_member_with_predicate(project_id, predicate=lambda e: e["user_id"] == user_id) + member = self._get_project_member_with_predicate(project_id, predicate=lambda e: e["user_id"] == user_id) + if member is None: + raise ValueError(f"project_member is not found. project_id='{project_id}', user_id='{user_id}'") + return member def get_user_id_from_account_id(self, project_id: str, account_id: str) -> str: """account_idからuser_idを取得する。 @@ -68,10 +80,7 @@ def get_user_id_from_account_id(self, project_id: str, account_id: str) -> str: Raises: ValueError: 指定したaccount_idのプロジェクトメンバが見つからない場合。 """ - member = self.get_project_member_from_account_id(project_id, account_id) - if member is None: - raise ValueError(f"project_member is not found. project_id='{project_id}', account_id='{account_id}'") - return member["user_id"] + return self.get_project_member_from_account_id(project_id, account_id)["user_id"] def get_account_id_from_user_id(self, project_id: str, user_id: str) -> str: """user_idからaccount_idを取得する。 @@ -86,7 +95,4 @@ def get_account_id_from_user_id(self, project_id: str, user_id: str) -> str: Raises: ValueError: 指定したuser_idのプロジェクトメンバが見つからない場合。 """ - member = self.get_project_member_from_user_id(project_id, user_id) - if member is None: - raise ValueError(f"project_member is not found. project_id='{project_id}', user_id='{user_id}'") - return member["account_id"] + return self.get_project_member_from_user_id(project_id, user_id)["account_id"] diff --git a/tests/test_project_member_repository.py b/tests/test_project_member_repository.py index 835f91ef..455b099d 100644 --- a/tests/test_project_member_repository.py +++ b/tests/test_project_member_repository.py @@ -25,6 +25,24 @@ def test_get_user_id_from_account_id__存在するaccount_idならuser_idを返 assert repository.get_user_id_from_account_id(PROJECT_ID, "account_id") == "user_id" +def test_get_project_member_from_account_id__存在するaccount_idならプロジェクトメンバを返す(): + member = { + "project_id": PROJECT_ID, + "account_id": "account_id", + "user_id": "user_id", + } + repository = create_repository_with_members([member]) + + assert repository.get_project_member_from_account_id(PROJECT_ID, "account_id") == member + + +def test_get_project_member_from_account_id__存在しないaccount_idならValueErrorを送出する(): + repository = create_repository_with_members([]) + + with pytest.raises(ValueError): + repository.get_project_member_from_account_id(PROJECT_ID, "unknown_account_id") + + def test_get_user_id_from_account_id__存在しないaccount_idならValueErrorを送出する(): repository = create_repository_with_members([]) @@ -46,6 +64,24 @@ def test_get_account_id_from_user_id__存在するuser_idならaccount_idを返 assert repository.get_account_id_from_user_id(PROJECT_ID, "user_id") == "account_id" +def test_get_project_member_from_user_id__存在するuser_idならプロジェクトメンバを返す(): + member = { + "project_id": PROJECT_ID, + "account_id": "account_id", + "user_id": "user_id", + } + repository = create_repository_with_members([member]) + + assert repository.get_project_member_from_user_id(PROJECT_ID, "user_id") == member + + +def test_get_project_member_from_user_id__存在しないuser_idならValueErrorを送出する(): + repository = create_repository_with_members([]) + + with pytest.raises(ValueError): + repository.get_project_member_from_user_id(PROJECT_ID, "unknown_user_id") + + def test_get_account_id_from_user_id__存在しないuser_idならValueErrorを送出する(): repository = create_repository_with_members([]) From 5ac384ad16d9cd75733af9af5e0963c4305d23f5 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Sun, 10 May 2026 00:54:15 +0900 Subject: [PATCH 5/9] =?UTF-8?q?`get=5Fmessage=5Fwith=5Flang`=20:=20?= =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=82=B1=E3=83=BC=E3=82=B9=E3=82=92?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E3=81=97=E3=80=81=E5=9E=8B=E3=81=AE=E7=84=A1?= =?UTF-8?q?=E8=A6=96=E3=82=92=E6=98=8E=E7=A4=BA=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- annofabapi/util/annotation_specs.py | 56 +++++++++++++++++-- annofabapi/wrapper.py | 41 ++++---------- docs/api_reference/index.rst | 3 +- tests/util/test_annotation_specs.py | 85 ++++++++++++++++++++++++++--- 4 files changed, 143 insertions(+), 42 deletions(-) diff --git a/annofabapi/util/annotation_specs.py b/annofabapi/util/annotation_specs.py index 749f1078..69a7ff5a 100644 --- a/annofabapi/util/annotation_specs.py +++ b/annofabapi/util/annotation_specs.py @@ -98,6 +98,54 @@ def get_message_with_lang(internationalization_message: InternationalizationMess return None +def get_label_name_en(label: LabelDefinition | dict[str, Any]) -> str: + """ + ラベル情報から英語名を取得します。 + + Args: + label: ラベル情報。キー ``label_name`` が存在している必要があります。 + + Returns: + ラベルの英語名。 + + Raises: + ValueError: 英語メッセージが見つからない場合 + """ + return get_english_message(label["label_name"]) + + +def get_attribute_name_en(attribute: AttributeDefinition | dict[str, Any]) -> str: + """ + 属性情報から英語名を取得します。 + + Args: + attribute: 属性情報。キー ``name`` が存在している必要があります。 + + Returns: + 属性の英語名。 + + Raises: + ValueError: 英語メッセージが見つからない場合 + """ + return get_english_message(attribute["name"]) + + +def get_choice_name_en(choice: AttributeChoice | dict[str, Any]) -> str: + """ + 選択肢情報から英語名を取得します。 + + Args: + choice: 選択肢情報。キー ``name`` が存在している必要があります。 + + Returns: + 選択肢の英語名。 + + Raises: + ValueError: 英語メッセージが見つからない場合 + """ + return get_english_message(choice["name"]) + + def get_choice(choices: list[AttributeChoice], *, choice_id: str | None = None, choice_name: str | None = None) -> AttributeChoice: """ 選択肢情報を取得します。 @@ -116,7 +164,7 @@ def get_choice(choices: list[AttributeChoice], *, choice_id: str | None = None, if choice_id is not None: result = [e for e in choices if e["choice_id"] == choice_id] elif choice_name is not None: - result = [e for e in choices if get_english_message(e["name"]) == choice_name] + result = [e for e in choices if get_choice_name_en(e) == choice_name] else: raise ValueError("'choice_id'か'choice_name'のどちらかはNone以外にしてください。") @@ -151,14 +199,14 @@ def get_attribute( if attribute_id is not None: result = [e for e in additionals if e["additional_data_definition_id"] == attribute_id] elif attribute_name is not None: - result = [e for e in additionals if get_english_message(e["name"]) == attribute_name] + result = [e for e in additionals if get_attribute_name_en(e) == attribute_name] else: raise ValueError("'attribute_id'か'attribute_name'のどちらかはNone以外にしてください。") label_name = None if label is not None: result = [e for e in result if e["additional_data_definition_id"] in label["additional_data_definitions"]] - label_name = get_english_message(label["label_name"]) + label_name = get_label_name_en(label) if len(result) == 0: raise ValueError( @@ -189,7 +237,7 @@ def get_label(labels: list[LabelDefinition], *, label_id: str | None = None, lab if label_id is not None: result = [e for e in labels if e["label_id"] == label_id] elif label_name is not None: - result = [e for e in labels if get_english_message(e["label_name"]) == label_name] + result = [e for e in labels if get_label_name_en(e) == label_name] else: raise ValueError("'label_id'か'label_name'のどちらかはNone以外にしてください。") diff --git a/annofabapi/wrapper.py b/annofabapi/wrapper.py index 20eada09..e2a91df7 100644 --- a/annofabapi/wrapper.py +++ b/annofabapi/wrapper.py @@ -47,6 +47,7 @@ TaskStatus, ) from annofabapi.parser import SimpleAnnotationDirParser, SimpleAnnotationParser +from annofabapi.util.annotation_specs import get_attribute_name_en, get_choice_name_en, get_label_name_en logger = logging.getLogger(__name__) @@ -533,19 +534,19 @@ def copy_annotation( def __get_label_info_from_label_name(self, label_name: str, annotation_specs_labels: list[LabelV1]) -> LabelV1 | None: for label in annotation_specs_labels: - if self.__get_label_name_en(label) == label_name: + if get_label_name_en(label) == label_name: return label return None def __get_additional_data_from_attribute_name(self, attribute_name: str, label_info: LabelV1) -> AdditionalDataDefinitionV1 | None: for additional_data in label_info["additional_data_definitions"]: - if self.__get_additional_data_definition_name_en(additional_data) == attribute_name: + if get_attribute_name_en(additional_data) == attribute_name: return additional_data return None def _get_choice_id_from_name(self, name: str, choices: list[dict[str, Any]]) -> str | None: - choice_info = more_itertools.first_true(choices, pred=lambda e: self.__get_choice_name_en(e) == name) + choice_info = more_itertools.first_true(choices, pred=lambda e: get_choice_name_en(e) == name) if choice_info is not None: return choice_info["choice_id"] else: @@ -572,7 +573,7 @@ def __to_additional_data_list(self, attributes: dict[str, Any], label_info: Labe if specs_additional_data is None: logger.warning( "アノテーション仕様の '%s' ラベルに、attribute_name='%s' である属性が存在しません。", - self.__get_label_name_en(label_info), + get_label_name_en(label_info), key, ) continue @@ -698,7 +699,7 @@ def to_label_v1(label_v2: dict[str, Any]) -> LabelV1: else: raise ValueError( f"additional_data_definition_id='{additional_data_definition_id}' に対応する属性情報が存在しません。" - f"label_id='{label_v2['label_id']}', label_name_en='{self.__get_label_name_en(label_v2)}'" + f"label_id='{label_v2['label_id']}', label_name_en='{get_label_name_en(label_v2)}'" ) label_v2["additional_data_definitions"] = new_additional_data_definitions return label_v2 @@ -784,24 +785,6 @@ def put_annotation_for_simple_annotation_json( # Public Method : AnnotationSpecs ######################################### - @staticmethod - def __get_label_name_en(label: dict[str, Any]) -> str: - """label情報から英語名を取得する""" - label_name_messages = label["label_name"]["messages"] - return next(e["message"] for e in label_name_messages if e["lang"] == "en-US") - - @staticmethod - def __get_additional_data_definition_name_en(additional_data_definition: dict[str, Any]) -> str: - """additional_data_definitionから英語名を取得する""" - messages = additional_data_definition["name"]["messages"] - return next(e["message"] for e in messages if e["lang"] == "en-US") - - @staticmethod - def __get_choice_name_en(choice: dict[str, Any]) -> str: - """choiceから英語名を取得する""" - messages = choice["name"]["messages"] - return next(e["message"] for e in messages if e["lang"] == "en-US") - def __get_dest_additional( self, src_additional: dict[str, Any], @@ -810,9 +793,9 @@ def __get_dest_additional( dest_labels: list[dict[str, Any]], dict_label_id: dict[str, str], ) -> dict[str, Any] | None: - src_additional_name_en = self.__get_additional_data_definition_name_en(src_additional) + src_additional_name_en = get_attribute_name_en(src_additional) for dest_additional in dest_additionals: - if src_additional_name_en != self.__get_additional_data_definition_name_en(dest_additional): + if src_additional_name_en != get_attribute_name_en(dest_additional): continue dest_label_contains_dest_additional = True @@ -861,10 +844,10 @@ def get_annotation_specs_relation(self, src_project_id: str, dest_project_id: st dict_label_id: dict[str, str] = {} for src_label in src_annotation_specs["labels"]: - src_label_name_en = self.__get_label_name_en(src_label) + src_label_name_en = get_label_name_en(src_label) dest_label = more_itertools.first_true( dest_labels, - pred=lambda e: self.__get_label_name_en(e) == src_label_name_en, # pylint: disable=cell-var-from-loop # noqa: B023 + pred=lambda e: get_label_name_en(e) == src_label_name_en, # pylint: disable=cell-var-from-loop # noqa: B023 ) if dest_label is not None: dict_label_id[src_label["label_id"]] = dest_label["label_id"] @@ -886,10 +869,10 @@ def get_annotation_specs_relation(self, src_project_id: str, dest_project_id: st dest_choices = dest_additional["choices"] for src_choice in src_additional["choices"]: - src_choice_name_en = self.__get_choice_name_en(src_choice) + src_choice_name_en = get_choice_name_en(src_choice) dest_choice = more_itertools.first_true( dest_choices, - pred=lambda e: self.__get_choice_name_en(e) == src_choice_name_en, # pylint: disable=cell-var-from-loop # noqa: B023 + pred=lambda e: get_choice_name_en(e) == src_choice_name_en, # pylint: disable=cell-var-from-loop # noqa: B023 ) if dest_choice is not None: dict_choice_id[ChoiceKey(src_additional["additional_data_definition_id"], src_choice["choice_id"])] = ChoiceKey( diff --git a/docs/api_reference/index.rst b/docs/api_reference/index.rst index 4595c2e3..4d523838 100644 --- a/docs/api_reference/index.rst +++ b/docs/api_reference/index.rst @@ -9,10 +9,11 @@ API reference wrapper resource parser - plugin dataclass exceptions segmentation + plugin + project_member_repository pydantic_models models util diff --git a/tests/util/test_annotation_specs.py b/tests/util/test_annotation_specs.py index 31b8a22a..abccc0ac 100644 --- a/tests/util/test_annotation_specs.py +++ b/tests/util/test_annotation_specs.py @@ -1,6 +1,16 @@ import pytest -from annofabapi.util.annotation_specs import AnnotationSpecsAccessor, AttributeChoice, Lang, get_choice, get_english_message, get_message_with_lang +from annofabapi.util.annotation_specs import ( + AnnotationSpecsAccessor, + AttributeChoice, + Lang, + get_attribute_name_en, + get_choice, + get_choice_name_en, + get_english_message, + get_label_name_en, + get_message_with_lang, +) class Test__get_english_message: @@ -31,6 +41,65 @@ def test__get_message_with_lang(self): assert get_message_with_lang(i18n_message, Lang.VI_VN) is None # type: ignore[arg-type] +class Test__get_label_name_en: + def test__get_label_name_en(self): + label = { + "label_id": "1", + "label_name": {"messages": [{"lang": "ja-JP", "message": "自動車"}, {"lang": "en-US", "message": "Car"}]}, + "additional_data_definitions": [], + } + + assert get_label_name_en(label) == "Car" + + def test__get_label_name_en__英語メッセージが存在しない場合はValueErrorをスローする(self): + label = { + "label_id": "1", + "label_name": {"messages": [{"lang": "ja-JP", "message": "自動車"}]}, + "additional_data_definitions": [], + } + + with pytest.raises(ValueError): + get_label_name_en(label) + + +class Test__get_attribute_name_en: + def test__get_attribute_name_en(self): + attribute = { + "additional_data_definition_id": "1", + "name": {"messages": [{"lang": "ja-JP", "message": "色"}, {"lang": "en-US", "message": "Color"}]}, + } + + assert get_attribute_name_en(attribute) == "Color" + + def test__get_attribute_name_en__英語メッセージが存在しない場合はValueErrorをスローする(self): + attribute = { + "additional_data_definition_id": "1", + "name": {"messages": [{"lang": "ja-JP", "message": "色"}]}, + } + + with pytest.raises(ValueError): + get_attribute_name_en(attribute) + + +class Test__get_choice_name_en: + def test__get_choice_name_en(self): + choice = { + "choice_id": "1", + "name": {"messages": [{"lang": "ja-JP", "message": "赤"}, {"lang": "en-US", "message": "Red"}]}, + } + + assert get_choice_name_en(choice) == "Red" + + def test__get_choice_name_en__英語メッセージが存在しない場合はValueErrorをスローする(self): + choice = { + "choice_id": "1", + "name": {"messages": [{"lang": "ja-JP", "message": "赤"}]}, + } + + with pytest.raises(ValueError): + get_choice_name_en(choice) + + class Test__AnnotationSpecsAccessor: def setup_method(self): self.annotation_specs = { @@ -48,12 +117,12 @@ def setup_method(self): def test_get_label_by_id(self): label = self.accessor.get_label(label_id="1") assert label["label_id"] == "1" - assert get_english_message(label["label_name"]) == "Car" + assert get_label_name_en(label) == "Car" def test_get_label_by_name(self): label = self.accessor.get_label(label_name="Bike") assert label["label_id"] == "2" - assert get_english_message(label["label_name"]) == "Bike" + assert get_label_name_en(label) == "Bike" def test_get_label_not_found(self): with pytest.raises(ValueError): @@ -62,12 +131,12 @@ def test_get_label_not_found(self): def test_get_attribute_by_id(self): attribute = self.accessor.get_attribute(attribute_id="1") assert attribute["additional_data_definition_id"] == "1" - assert get_english_message(attribute["name"]) == "Color" + assert get_attribute_name_en(attribute) == "Color" def test_get_attribute_by_name(self): attribute = self.accessor.get_attribute(attribute_name="Size") assert attribute["additional_data_definition_id"] == "2" - assert get_english_message(attribute["name"]) == "Size" + assert get_attribute_name_en(attribute) == "Size" def test_get_attribute_not_found(self): with pytest.raises(ValueError): @@ -77,7 +146,7 @@ def test_get_attribute_by_id_and_label(self): label = self.accessor.get_label(label_id="1") attribute = self.accessor.get_attribute(attribute_id="1", label=label) assert attribute["additional_data_definition_id"] == "1" - assert get_english_message(attribute["name"]) == "Color" + assert get_attribute_name_en(attribute) == "Color" def test_get_attribute_by_id_and_label__not_found(self): label = self.accessor.get_label(label_id="2") @@ -95,12 +164,12 @@ def setup_method(self): def test_get_choice_by_id(self): choice = get_choice(self.choices, choice_id="1") assert choice["choice_id"] == "1" - assert get_english_message(choice["name"]) == "Option1" + assert get_choice_name_en(choice) == "Option1" def test_get_choice_by_name(self): choice = get_choice(self.choices, choice_name="Option2") assert choice["choice_id"] == "2" - assert get_english_message(choice["name"]) == "Option2" + assert get_choice_name_en(choice) == "Option2" def test_get_choice_not_found(self): with pytest.raises(ValueError): From 83841fde8c0bb6bcd6070d0df5468e55afd03d26 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Sun, 10 May 2026 01:00:52 +0900 Subject: [PATCH 6/9] Use minimal typed dicts for annotation name helpers --- annofabapi/util/annotation_specs.py | 18 +++++++++++++++--- annofabapi/wrapper.py | 16 ++++++++-------- tests/util/test_annotation_specs.py | 22 ++++++++-------------- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/annofabapi/util/annotation_specs.py b/annofabapi/util/annotation_specs.py index 69a7ff5a..e42c62df 100644 --- a/annofabapi/util/annotation_specs.py +++ b/annofabapi/util/annotation_specs.py @@ -43,6 +43,18 @@ class LabelDefinition(TypedDict): additional_data_definitions: list[str] +class LabelNameHolder(TypedDict): + """ラベル名を持つ情報です。""" + + label_name: InternationalizationMessage + + +class NameHolder(TypedDict): + """多言語化された名前を持つ情報です。""" + + name: InternationalizationMessage + + def get_english_message(internationalization_message: InternationalizationMessage | dict[str, Any]) -> str: """ `InternationalizationMessage`クラスの値から、英語メッセージを取得します。 @@ -98,7 +110,7 @@ def get_message_with_lang(internationalization_message: InternationalizationMess return None -def get_label_name_en(label: LabelDefinition | dict[str, Any]) -> str: +def get_label_name_en(label: LabelNameHolder) -> str: """ ラベル情報から英語名を取得します。 @@ -114,7 +126,7 @@ def get_label_name_en(label: LabelDefinition | dict[str, Any]) -> str: return get_english_message(label["label_name"]) -def get_attribute_name_en(attribute: AttributeDefinition | dict[str, Any]) -> str: +def get_attribute_name_en(attribute: NameHolder) -> str: """ 属性情報から英語名を取得します。 @@ -130,7 +142,7 @@ def get_attribute_name_en(attribute: AttributeDefinition | dict[str, Any]) -> st return get_english_message(attribute["name"]) -def get_choice_name_en(choice: AttributeChoice | dict[str, Any]) -> str: +def get_choice_name_en(choice: NameHolder) -> str: """ 選択肢情報から英語名を取得します。 diff --git a/annofabapi/wrapper.py b/annofabapi/wrapper.py index e2a91df7..f5d41eba 100644 --- a/annofabapi/wrapper.py +++ b/annofabapi/wrapper.py @@ -14,7 +14,7 @@ from collections.abc import Callable from dataclasses import dataclass from pathlib import Path -from typing import Any +from typing import Any, cast import more_itertools import requests @@ -47,7 +47,7 @@ TaskStatus, ) from annofabapi.parser import SimpleAnnotationDirParser, SimpleAnnotationParser -from annofabapi.util.annotation_specs import get_attribute_name_en, get_choice_name_en, get_label_name_en +from annofabapi.util.annotation_specs import LabelNameHolder, NameHolder, get_attribute_name_en, get_choice_name_en, get_label_name_en logger = logging.getLogger(__name__) @@ -534,7 +534,7 @@ def copy_annotation( def __get_label_info_from_label_name(self, label_name: str, annotation_specs_labels: list[LabelV1]) -> LabelV1 | None: for label in annotation_specs_labels: - if get_label_name_en(label) == label_name: + if get_label_name_en(cast(LabelNameHolder, label)) == label_name: return label return None @@ -546,7 +546,7 @@ def __get_additional_data_from_attribute_name(self, attribute_name: str, label_i return None def _get_choice_id_from_name(self, name: str, choices: list[dict[str, Any]]) -> str | None: - choice_info = more_itertools.first_true(choices, pred=lambda e: get_choice_name_en(e) == name) + choice_info = more_itertools.first_true(choices, pred=lambda e: get_choice_name_en(cast(NameHolder, e)) == name) if choice_info is not None: return choice_info["choice_id"] else: @@ -573,7 +573,7 @@ def __to_additional_data_list(self, attributes: dict[str, Any], label_info: Labe if specs_additional_data is None: logger.warning( "アノテーション仕様の '%s' ラベルに、attribute_name='%s' である属性が存在しません。", - get_label_name_en(label_info), + get_label_name_en(cast(LabelNameHolder, label_info)), key, ) continue @@ -699,7 +699,7 @@ def to_label_v1(label_v2: dict[str, Any]) -> LabelV1: else: raise ValueError( f"additional_data_definition_id='{additional_data_definition_id}' に対応する属性情報が存在しません。" - f"label_id='{label_v2['label_id']}', label_name_en='{get_label_name_en(label_v2)}'" + f"label_id='{label_v2['label_id']}', label_name_en='{get_label_name_en(cast(LabelNameHolder, label_v2))}'" ) label_v2["additional_data_definitions"] = new_additional_data_definitions return label_v2 @@ -793,9 +793,9 @@ def __get_dest_additional( dest_labels: list[dict[str, Any]], dict_label_id: dict[str, str], ) -> dict[str, Any] | None: - src_additional_name_en = get_attribute_name_en(src_additional) + src_additional_name_en = get_attribute_name_en(cast(NameHolder, src_additional)) for dest_additional in dest_additionals: - if src_additional_name_en != get_attribute_name_en(dest_additional): + if src_additional_name_en != get_attribute_name_en(cast(NameHolder, dest_additional)): continue dest_label_contains_dest_additional = True diff --git a/tests/util/test_annotation_specs.py b/tests/util/test_annotation_specs.py index abccc0ac..1d30baca 100644 --- a/tests/util/test_annotation_specs.py +++ b/tests/util/test_annotation_specs.py @@ -3,7 +3,9 @@ from annofabapi.util.annotation_specs import ( AnnotationSpecsAccessor, AttributeChoice, + LabelNameHolder, Lang, + NameHolder, get_attribute_name_en, get_choice, get_choice_name_en, @@ -43,19 +45,15 @@ def test__get_message_with_lang(self): class Test__get_label_name_en: def test__get_label_name_en(self): - label = { - "label_id": "1", + label: LabelNameHolder = { "label_name": {"messages": [{"lang": "ja-JP", "message": "自動車"}, {"lang": "en-US", "message": "Car"}]}, - "additional_data_definitions": [], } assert get_label_name_en(label) == "Car" def test__get_label_name_en__英語メッセージが存在しない場合はValueErrorをスローする(self): - label = { - "label_id": "1", + label: LabelNameHolder = { "label_name": {"messages": [{"lang": "ja-JP", "message": "自動車"}]}, - "additional_data_definitions": [], } with pytest.raises(ValueError): @@ -64,16 +62,14 @@ def test__get_label_name_en__英語メッセージが存在しない場合はVal class Test__get_attribute_name_en: def test__get_attribute_name_en(self): - attribute = { - "additional_data_definition_id": "1", + attribute: NameHolder = { "name": {"messages": [{"lang": "ja-JP", "message": "色"}, {"lang": "en-US", "message": "Color"}]}, } assert get_attribute_name_en(attribute) == "Color" def test__get_attribute_name_en__英語メッセージが存在しない場合はValueErrorをスローする(self): - attribute = { - "additional_data_definition_id": "1", + attribute: NameHolder = { "name": {"messages": [{"lang": "ja-JP", "message": "色"}]}, } @@ -83,16 +79,14 @@ def test__get_attribute_name_en__英語メッセージが存在しない場合 class Test__get_choice_name_en: def test__get_choice_name_en(self): - choice = { - "choice_id": "1", + choice: NameHolder = { "name": {"messages": [{"lang": "ja-JP", "message": "赤"}, {"lang": "en-US", "message": "Red"}]}, } assert get_choice_name_en(choice) == "Red" def test__get_choice_name_en__英語メッセージが存在しない場合はValueErrorをスローする(self): - choice = { - "choice_id": "1", + choice: NameHolder = { "name": {"messages": [{"lang": "ja-JP", "message": "赤"}]}, } From c777c2e756fe082f46558e6f98e4378216aa0802 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Sun, 10 May 2026 01:18:03 +0900 Subject: [PATCH 7/9] Reduce casts for annotation spec name helpers --- annofabapi/util/annotation_specs.py | 13 +++++++------ annofabapi/wrapper.py | 16 ++++++++-------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/annofabapi/util/annotation_specs.py b/annofabapi/util/annotation_specs.py index e42c62df..5740f781 100644 --- a/annofabapi/util/annotation_specs.py +++ b/annofabapi/util/annotation_specs.py @@ -1,4 +1,5 @@ -from typing import Any, Literal, TypedDict, cast +from collections.abc import Mapping +from typing import Any, Literal, TypedDict import more_itertools @@ -55,7 +56,7 @@ class NameHolder(TypedDict): name: InternationalizationMessage -def get_english_message(internationalization_message: InternationalizationMessage | dict[str, Any]) -> str: +def get_english_message(internationalization_message: Mapping[str, Any]) -> str: """ `InternationalizationMessage`クラスの値から、英語メッセージを取得します。 英語メッセージが見つからない場合は ``ValueError`` をスローします。 @@ -72,7 +73,7 @@ def get_english_message(internationalization_message: InternationalizationMessag Raises: ValueError: 英語メッセージが見つからない場合 """ - messages = cast(list[InternationalizationMessageItem], internationalization_message["messages"]) + messages = internationalization_message["messages"] result = more_itertools.first_true(messages, pred=lambda e: e["lang"] == Lang.EN_US.value) if result is not None: return result["message"] @@ -110,7 +111,7 @@ def get_message_with_lang(internationalization_message: InternationalizationMess return None -def get_label_name_en(label: LabelNameHolder) -> str: +def get_label_name_en(label: Mapping[str, Any]) -> str: """ ラベル情報から英語名を取得します。 @@ -126,7 +127,7 @@ def get_label_name_en(label: LabelNameHolder) -> str: return get_english_message(label["label_name"]) -def get_attribute_name_en(attribute: NameHolder) -> str: +def get_attribute_name_en(attribute: Mapping[str, Any]) -> str: """ 属性情報から英語名を取得します。 @@ -142,7 +143,7 @@ def get_attribute_name_en(attribute: NameHolder) -> str: return get_english_message(attribute["name"]) -def get_choice_name_en(choice: NameHolder) -> str: +def get_choice_name_en(choice: Mapping[str, Any]) -> str: """ 選択肢情報から英語名を取得します。 diff --git a/annofabapi/wrapper.py b/annofabapi/wrapper.py index f5d41eba..e2a91df7 100644 --- a/annofabapi/wrapper.py +++ b/annofabapi/wrapper.py @@ -14,7 +14,7 @@ from collections.abc import Callable from dataclasses import dataclass from pathlib import Path -from typing import Any, cast +from typing import Any import more_itertools import requests @@ -47,7 +47,7 @@ TaskStatus, ) from annofabapi.parser import SimpleAnnotationDirParser, SimpleAnnotationParser -from annofabapi.util.annotation_specs import LabelNameHolder, NameHolder, get_attribute_name_en, get_choice_name_en, get_label_name_en +from annofabapi.util.annotation_specs import get_attribute_name_en, get_choice_name_en, get_label_name_en logger = logging.getLogger(__name__) @@ -534,7 +534,7 @@ def copy_annotation( def __get_label_info_from_label_name(self, label_name: str, annotation_specs_labels: list[LabelV1]) -> LabelV1 | None: for label in annotation_specs_labels: - if get_label_name_en(cast(LabelNameHolder, label)) == label_name: + if get_label_name_en(label) == label_name: return label return None @@ -546,7 +546,7 @@ def __get_additional_data_from_attribute_name(self, attribute_name: str, label_i return None def _get_choice_id_from_name(self, name: str, choices: list[dict[str, Any]]) -> str | None: - choice_info = more_itertools.first_true(choices, pred=lambda e: get_choice_name_en(cast(NameHolder, e)) == name) + choice_info = more_itertools.first_true(choices, pred=lambda e: get_choice_name_en(e) == name) if choice_info is not None: return choice_info["choice_id"] else: @@ -573,7 +573,7 @@ def __to_additional_data_list(self, attributes: dict[str, Any], label_info: Labe if specs_additional_data is None: logger.warning( "アノテーション仕様の '%s' ラベルに、attribute_name='%s' である属性が存在しません。", - get_label_name_en(cast(LabelNameHolder, label_info)), + get_label_name_en(label_info), key, ) continue @@ -699,7 +699,7 @@ def to_label_v1(label_v2: dict[str, Any]) -> LabelV1: else: raise ValueError( f"additional_data_definition_id='{additional_data_definition_id}' に対応する属性情報が存在しません。" - f"label_id='{label_v2['label_id']}', label_name_en='{get_label_name_en(cast(LabelNameHolder, label_v2))}'" + f"label_id='{label_v2['label_id']}', label_name_en='{get_label_name_en(label_v2)}'" ) label_v2["additional_data_definitions"] = new_additional_data_definitions return label_v2 @@ -793,9 +793,9 @@ def __get_dest_additional( dest_labels: list[dict[str, Any]], dict_label_id: dict[str, str], ) -> dict[str, Any] | None: - src_additional_name_en = get_attribute_name_en(cast(NameHolder, src_additional)) + src_additional_name_en = get_attribute_name_en(src_additional) for dest_additional in dest_additionals: - if src_additional_name_en != get_attribute_name_en(cast(NameHolder, dest_additional)): + if src_additional_name_en != get_attribute_name_en(dest_additional): continue dest_label_contains_dest_additional = True From 394e34159570fed5ae9b52b1e77c8c54500fae49 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Sun, 10 May 2026 01:27:28 +0900 Subject: [PATCH 8/9] Remove trailing whitespace in index.rst and add project_member_repository.rst documentation --- docs/api_reference/index.rst | 4 +--- docs/api_reference/project_member_repository.rst | 7 +++++++ 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 docs/api_reference/project_member_repository.rst diff --git a/docs/api_reference/index.rst b/docs/api_reference/index.rst index 4d523838..685d8cdd 100644 --- a/docs/api_reference/index.rst +++ b/docs/api_reference/index.rst @@ -12,12 +12,10 @@ API reference dataclass exceptions segmentation - plugin + plugin project_member_repository pydantic_models models util utils credentials - - diff --git a/docs/api_reference/project_member_repository.rst b/docs/api_reference/project_member_repository.rst new file mode 100644 index 00000000..a447856e --- /dev/null +++ b/docs/api_reference/project_member_repository.rst @@ -0,0 +1,7 @@ +annofabapi.project_member_repository module +=========================================== + + + +.. automodule:: annofabapi.project_member_repository + :members: From 7d5cff5c24254343d3d771454ec132fb1212bc76 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Sun, 10 May 2026 01:29:56 +0900 Subject: [PATCH 9/9] =?UTF-8?q?index.rst:=20pydantic=5Fmodels=E3=81=A8mode?= =?UTF-8?q?ls=E3=81=AE=E9=A0=86=E5=BA=8F=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api_reference/index.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/api_reference/index.rst b/docs/api_reference/index.rst index 685d8cdd..9babc7e0 100644 --- a/docs/api_reference/index.rst +++ b/docs/api_reference/index.rst @@ -14,8 +14,7 @@ API reference segmentation plugin project_member_repository - pydantic_models - models util utils - credentials + pydantic_models + models