From f77cfc548d45910a734715a39ad655eb5baf73d0 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Wed, 4 Sep 2019 15:39:27 +0900 Subject: [PATCH 1/5] =?UTF-8?q?=E3=83=97=E3=83=AD=E3=82=B8=E3=82=A7?= =?UTF-8?q?=E3=82=AF=E3=83=88=E3=81=AE=E3=82=B3=E3=83=94=E3=83=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Pipfile.lock | 30 ++--- .../annotation/list_annotation_count.py | 2 +- annofabcli/common/cli.py | 22 ++-- annofabcli/common/exceptions.py | 16 ++- annofabcli/common/facade.py | 24 +++- annofabcli/input_data/list_input_data.py | 2 +- annofabcli/project/copy_project.py | 107 ++++++++++++++++++ .../project_member/copy_project_members.py | 4 +- annofabcli/task/list_tasks.py | 2 +- setup.py | 2 +- 10 files changed, 178 insertions(+), 33 deletions(-) create mode 100644 annofabcli/project/copy_project.py diff --git a/Pipfile.lock b/Pipfile.lock index 296e4a3b..56e1bb70 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -17,10 +17,10 @@ "develop": { "annofabapi": { "hashes": [ - "sha256:089b51e95065d5d57814cd56ac7b325c22e46be8fb7f7b6c0afda1eb23eb9fa8", - "sha256:134de5a286f2c4c030e402f61d2da09d3472b967a08a79bbe98c2c9039c75076" + "sha256:257ee111e7e86bd726b99a84c4e9c48dde8d004574bc6596a43bd098dc90a42c", + "sha256:b6517e217ea4ec1d5de96dfb6c3460ab37a7b1fea8b95bee0f0bbf588197bf16" ], - "version": "==0.14.0" + "version": "==0.15.3" }, "annofabcli": { "editable": true, @@ -123,10 +123,10 @@ }, "dataclasses-json": { "hashes": [ - "sha256:a439f95ed7c530f7709923ef72944c0e8fd3d5c7e9e9b76b8416e1a9edd6700b", - "sha256:e16dec16c87d1b1a58c139aa7945f27987040be39b4262cd785050dc4eb8d4a2" + "sha256:065a94e07599b28830b55ac17e3a26c49ef88e209513140f2ef1a1954dd303c6", + "sha256:d5883036560707fb785bc705fd965a0c9b94b44d10acca8ddde9dcc40dc12d61" ], - "version": "==0.3.0" + "version": "==0.3.2" }, "dictdiffer": { "hashes": [ @@ -167,11 +167,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:23d3d873e008a513952355379d93cbcab874c58f4f034ff657c7a87422fa64e8", - "sha256:80d2de76188eabfbfcf27e6a37342c2827801e59c4cc14b0371c56fed43820e3" + "sha256:9ff1b1c5a354142de080b8a4e9803e5d0d59283c93aed808617c787d16768375", + "sha256:b7143592e374e50584564794fcb8aaf00a23025f9db866627f89a21491847a8d" ], "markers": "python_version < '3.8'", - "version": "==0.19" + "version": "==0.20" }, "isort": { "hashes": [ @@ -240,10 +240,10 @@ }, "marshmallow": { "hashes": [ - "sha256:5e0b15f33c6e227b51e1fbe29a306676686f3c3a3bf7fe0aca005144c3e74d7c", - "sha256:8c6a22cfc9ca33945e2707c771c44030a035be96082a18643d431280b9b8f08e" + "sha256:23f684b54b1955ebd5bdfbdda4062e438ef86218f14f1a356f570cdf0c016ab3", + "sha256:fcfc9ffd75a883da06f30f604a4e81dd0b56eb9438f4d0a8de6bbaa163ce9ec3" ], - "version": "==3.0.0rc6" + "version": "==3.0.1" }, "marshmallow-enum": { "hashes": [ @@ -440,11 +440,11 @@ }, "pytest": { "hashes": [ - "sha256:95b1f6db806e5b1b5b443efeb58984c24945508f93a866c1719e1a507a957d7c", - "sha256:c3d5020755f70c82eceda3feaf556af9a341334414a8eca521a18f463bcead88" + "sha256:95d13143cc14174ca1a01ec68e84d76ba5d9d493ac02716fd9706c949a505210", + "sha256:b78fe2881323bd44fd9bd76e5317173d4316577e7b1cddebae9136a4495ec865" ], "index": "pypi", - "version": "==5.1.1" + "version": "==5.1.2" }, "pytest-cov": { "hashes": [ diff --git a/annofabcli/annotation/list_annotation_count.py b/annofabcli/annotation/list_annotation_count.py index d731df95..707d9952 100644 --- a/annofabcli/annotation/list_annotation_count.py +++ b/annofabcli/annotation/list_annotation_count.py @@ -165,7 +165,7 @@ def list_annotations(self, project_id: str, annotation_query: Dict[str, Any], gr アノテーション一覧を出力する """ - super().validate_project(project_id, roles=None) + super().validate_project(project_id, project_member_roles=None) all_annotations = [] if len(task_id_list) > 0: diff --git a/annofabcli/common/cli.py b/annofabcli/common/cli.py index b912cf2c..803fff9b 100644 --- a/annofabcli/common/cli.py +++ b/annofabcli/common/cli.py @@ -14,11 +14,11 @@ import pandas import requests from annofabapi.exceptions import AnnofabApiException -from annofabapi.models import ProjectMemberRole # pylint: disable=unused-import +from annofabapi.models import ProjectMemberRole, OrganizationMemberRole import annofabcli from annofabcli.common.enums import FormatArgument -from annofabcli.common.exceptions import AuthorizationError +from annofabcli.common.exceptions import AuthorizationError, ProjectAuthorizationError, OrganizationAuthorizationError from annofabcli.common.facade import AnnofabApiFacade from annofabcli.common.typing import InputDataSize @@ -362,12 +362,13 @@ def process_common_args(self, args: argparse.Namespace): logger.info(f"args: {args}") - def validate_project(self, project_id, roles: Optional[List[ProjectMemberRole]] = None): + def validate_project(self, project_id, project_member_roles: Optional[List[ProjectMemberRole]] = None, organization_member_roles: Optional[List[OrganizationMemberRole]] = None): """ - プロジェクトに対する権限が付与されているかを確認する。 + プロジェクト or 組織に対して、必要な権限が付与されているかを確認する。 Args: project_id:  - roles: Roleの一覧。 + project_member_roles: プロジェクトメンバロールの一覧 + organization_member_roles: 組織メンバロールの一覧 Raises: AuthorizationError: 自分自身のRoleがいずれかのRoleにも合致しなければ、AuthorizationErrorが発生する。 @@ -376,9 +377,14 @@ def validate_project(self, project_id, roles: Optional[List[ProjectMemberRole]] project_title = self.facade.get_project_title(project_id) logger.info(f"project_title = {project_title}, project_id = {project_id}") - if roles is not None: - if not self.facade.contains_anys_role(project_id, roles): - raise AuthorizationError(project_title, roles) + if project_member_roles is not None: + if not self.facade.contains_any_project_member_role(project_id, project_member_roles): + raise ProjectAuthorizationError(project_title, project_member_roles) + + if organization_member_roles is not None: + organization_name= self.facade.get_organization_name_from_project_id(project_id) + if not self.facade.contains_any_organization_member_role(organization_name, organization_member_roles): + raise OrganizationAuthorizationError(organization_name, organization_member_roles) def confirm_processing(self, confirm_message: str) -> bool: """ diff --git a/annofabcli/common/exceptions.py b/annofabcli/common/exceptions.py index 1531828d..d8f30cd1 100644 --- a/annofabcli/common/exceptions.py +++ b/annofabcli/common/exceptions.py @@ -6,7 +6,7 @@ from typing import List, Optional # pylint: disable=unused-import -from annofabapi.models import ProjectMemberRole +from annofabapi.models import ProjectMemberRole, OrganizationMemberRole class AnnofabCliException(Exception): @@ -25,10 +25,22 @@ def __init__(self, loing_user_id: str): class AuthorizationError(AnnofabCliException): + pass + +class ProjectAuthorizationError(AuthorizationError): """ - AnnoFabの認可エラー + AnnoFabプロジェクトに関する認可エラー """ def __init__(self, project_title: str, roles: List[ProjectMemberRole]): role_values = [e.value for e in roles] msg = f"プロジェクト: {project_title} に、ロール: {role_values} のいずれかが付与されていません。" super().__init__(msg) + +class OrganizationAuthorizationError(AuthorizationError): + """ + AnnoFab組織に関する認可エラー + """ + def __init__(self, organization_name: str, roles: List[OrganizationMemberRole]): + role_values = [e.value for e in roles] + msg = f"組織: {organization_name} に、ロール: {role_values} のいずれかが付与されていません。" + super().__init__(msg) diff --git a/annofabcli/common/facade.py b/annofabcli/common/facade.py index 05c56b88..4601acd6 100644 --- a/annofabcli/common/facade.py +++ b/annofabcli/common/facade.py @@ -9,7 +9,7 @@ import annofabapi import annofabapi.utils import more_itertools -from annofabapi.models import OrganizationMember, ProjectId, ProjectMemberRole +from annofabapi.models import OrganizationMember, ProjectId, ProjectMemberRole, OrganizationMemberRole logger = logging.getLogger(__name__) @@ -194,7 +194,7 @@ def my_role_is_owner(self, project_id: str) -> bool: my_member, _ = self.service.api.get_my_member_in_project(project_id) return my_member["member_role"] == "owner" - def contains_anys_role(self, project_id: str, roles: List[ProjectMemberRole]) -> bool: + def contains_any_project_member_role(self, project_id: str, roles: List[ProjectMemberRole]) -> bool: """ 自分自身のプロジェクトメンバとしてのロールが、指定されたロールのいずれかに合致するかどうか Args: @@ -209,6 +209,22 @@ def contains_anys_role(self, project_id: str, roles: List[ProjectMemberRole]) -> my_role = ProjectMemberRole(my_member["member_role"]) return my_role in roles + def contains_any_organization_member_role(self, organization_name: str, roles: List[OrganizationMemberRole]) -> bool: + """ + 自分自身の組織メンバとしてのロールが、指定されたロールのいずれかに合致するかどうか + Args: + organization_name: 組織名 + roles: ロール一覧 + + Returns: + Trueなら、自分自身のロールが、指定されたロールのいずれかに合致する。 + + """ + my_organizations = self.service.wrapper.get_all_my_organizations() + organization = more_itertools.first_true(my_organizations, pred=lambda e: e["name"] == organization_name) + my_role = OrganizationMemberRole(organization["my_role"]) + return my_role in roles + def _download_annotation_archive_with_waiting(self, project_id: str, dest_path: str, download_func: Callable[[str, str], Any], job_access_interval: int, max_job_access: int) -> bool: @@ -222,6 +238,10 @@ def get_latest_job(): job_access_count = 0 while True: job = get_latest_job() + if job_access_count == 0 and job["job_status"] != "progress": + logger.debug(f"進行中のジョブはありませんでした。") + return True + job_access_count += 1 if job["job_status"] == "succeeded": diff --git a/annofabcli/input_data/list_input_data.py b/annofabcli/input_data/list_input_data.py index 379467b3..6ecc1be0 100644 --- a/annofabcli/input_data/list_input_data.py +++ b/annofabcli/input_data/list_input_data.py @@ -100,7 +100,7 @@ def print_input_data(self, project_id: str, input_data_query: Dict[str, Any], ad """ - super().validate_project(project_id, roles=None) + super().validate_project(project_id, project_member_roles=None) input_data_list = self.get_input_data(project_id, input_data_query, add_details) input_data_list = self.search_with_jmespath_expression(input_data_list) diff --git a/annofabcli/project/copy_project.py b/annofabcli/project/copy_project.py new file mode 100644 index 00000000..30c06cdc --- /dev/null +++ b/annofabcli/project/copy_project.py @@ -0,0 +1,107 @@ +import argparse +import logging +from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import +import uuid +import annofabapi +from annofabapi.models import OrganizationMember, ProjectMember, ProjectMemberRole, OrganizationMemberRole + +import annofabcli +import copy +from annofabcli import AnnofabApiFacade +from annofabcli.common.cli import AbstractCommandLineInterface, build_annofabapi_resource_and_login +from annofabcli.common.cli import AbstractCommandLineInterface, ArgumentParser, build_annofabapi_resource_and_login + + +logger = logging.getLogger(__name__) + + +class CopyProject(AbstractCommandLineInterface): + """ + プロジェクトをコピーする + """ + @staticmethod + def find_member(members: List[Dict[str, Any]], account_id: str) -> Optional[Dict[str, Any]]: + for m in members: + if m["account_id"] == account_id: + return m + + return None + + def copy_project(self, src_project_id: str, dest_project_id: str, dest_title: str, + dest_overview: Optional[str] = None, copy_options: Optional[Dict[str, bool]] = None, wait_for_completion:bool = False): + """ + プロジェクトメンバを、別のプロジェクトにコピーする。 + + Args: + src_project_id: コピー元のproject_id + dest_project_id: コピー先のproject_id + delete_dest: Trueならばコピー先にしか存在しないプロジェクトメンバを削除する。 + + """ + + self.validate_project(src_project_id, project_member_roles=[ProjectMemberRole.OWNER], organization_member_roles=[OrganizationMemberRole.ADMINISTRATOR, OrganizationMemberRole.OWNER]) + + src_project_title = self.facade.get_project_title(src_project_id) + + confirm_message = f"{src_project_title} ({src_project_id} を、{dest_title} ({dest_project_id}) にコピーしますか?" + if not self.confirm_processing(confirm_message): + return + + if copy_options is not None: + query_params: Dict[str, Any] = copy.deepcopy(copy_options) + else: + query_params: Dict[str, Any] = {} + + query_params.update({ + "dest_project_id": dest_project_id, + "dest_title": dest_title, + "dest_overview": dest_overview + }) + + self.service.api.initiate_project_copy(src_project_id, query_params=query_params) + logger.info(f"プロジェクトのコピーを実施しています。") + + + def main(self): + args = self.args + dest_project_id = args.dest_project_id if args.dest_project_id is not None else uuid.uuid4() + self.copy_project(args.src_project_id, args.dest_project_id, delete_dest=args.delete_dest) + + +def main(args): + service = build_annofabapi_resource_and_login() + facade = AnnofabApiFacade(service) + CopyProject(service, facade, args).main() + + +def parse_args(parser: argparse.ArgumentParser): + argument_parser = ArgumentParser(parser) + + argument_parser.add_project_id(help_message='コピー元のプロジェクトのproject_idを指定してください。') + + + parser.add_argument('--dest_project_id', type=str, + help='新しいプロジェクトのproject_idを指定してください。省略した場合は UUIDv4 フォーマットになります。') + parser.add_argument('--dest_title', type=str, required=True, help="新しいプロジェクトのタイトルを指定してください。") + parser.add_argument('--dest_overview', type=str, help="新しいプロジェクトの概要を指定してください。") + + parser.add_argument('--copy_inputs', action='store_true', help="「入力データ」をコピーするかどうかを指定します。") + parser.add_argument('--copy_tasks', action='store_true', help="「タスク」をコピーするかどうかを指定します。") + parser.add_argument('--copy_annotations', action='store_true', help="「アノテーション」をコピーするかどうかを指定します。") + parser.add_argument('--copy_webhooks', action='store_true', help="「Webhook」をコピーするかどうかを指定します。") + parser.add_argument('--copy_supplementaly_data', action='store_true', help="「補助情報」をコピーするかどうかを指定します。") + parser.add_argument('--copy_instructions', action='store_true', help="「作業ガイド」をコピーするかどうかを指定します。") + + parser.add_argument('--wait_for_completion', action='store_true', help="プロジェクトのコピーが完了するまで待ちます。") + + parser.set_defaults(subcommand_func=main) + + +def add_parser(subparsers: argparse._SubParsersAction): + subcommand_name = "copy" + subcommand_help = "プロジェクトをコピーします。" + description = ("プロジェクトをコピーして(アノテーション仕様やメンバーを引き継いで)、新しいプロジェクトを作成します。") + epilog = "コピー元のプロジェクトに対してオーナロール、組織に対して組織管理者、組織オーナを持つユーザで実行してください。" + + parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog) + parse_args(parser) diff --git a/annofabcli/project_member/copy_project_members.py b/annofabcli/project_member/copy_project_members.py index 7dc81eca..6dd88c05 100644 --- a/annofabcli/project_member/copy_project_members.py +++ b/annofabcli/project_member/copy_project_members.py @@ -36,8 +36,8 @@ def validate_projects(self, src_project_id: str, dest_project_id: str): """ - super().validate_project(src_project_id, roles=None) - super().validate_project(dest_project_id, roles=[ProjectMemberRole.OWNER]) + super().validate_project(src_project_id, project_member_roles=None) + super().validate_project(dest_project_id, project_member_roles=[ProjectMemberRole.OWNER]) def get_organization_members_from_project_id(self, project_id: str) -> List[OrganizationMember]: organization_name = self.facade.get_organization_name_from_project_id(project_id) diff --git a/annofabcli/task/list_tasks.py b/annofabcli/task/list_tasks.py index 5043157c..c788d2fe 100644 --- a/annofabcli/task/list_tasks.py +++ b/annofabcli/task/list_tasks.py @@ -93,7 +93,7 @@ def print_tasks(self, project_id: str, task_query: Dict[str, Any], arg_format: s """ - super().validate_project(project_id, roles=None) + super().validate_project(project_id, project_member_roles=None) tasks = self.get_tasks(project_id, task_query) tasks = self.search_with_jmespath_expression(tasks) diff --git a/setup.py b/setup.py index bf57e8fa..d138126c 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ maintainer='yuji38kwmt', license='MIT', keywords='annofab api cli', url='https://github.com/kurusugawa-computer/annofab-cli', - install_requires=['annofabapi>=0.13.1', + install_requires=['annofabapi>=0.15.3', 'requests', 'pillow', 'pyyaml', From 5cf5660cda869eb252a3753cf2a33ac18d285ed1 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Wed, 4 Sep 2019 16:22:51 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=E3=83=97=E3=83=AD=E3=82=B8=E3=82=A7?= =?UTF-8?q?=E3=82=AF=E3=83=88=E3=81=AE=E3=82=B3=E3=83=94=E3=83=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- annofabcli/common/cli.py | 9 +-- annofabcli/common/exceptions.py | 4 +- annofabcli/common/facade.py | 5 +- annofabcli/project/copy_project.py | 75 ++++++++++++++---------- annofabcli/project/subcommand_project.py | 2 + 5 files changed, 58 insertions(+), 37 deletions(-) diff --git a/annofabcli/common/cli.py b/annofabcli/common/cli.py index 803fff9b..c09db5d9 100644 --- a/annofabcli/common/cli.py +++ b/annofabcli/common/cli.py @@ -14,11 +14,11 @@ import pandas import requests from annofabapi.exceptions import AnnofabApiException -from annofabapi.models import ProjectMemberRole, OrganizationMemberRole +from annofabapi.models import OrganizationMemberRole, ProjectMemberRole import annofabcli from annofabcli.common.enums import FormatArgument -from annofabcli.common.exceptions import AuthorizationError, ProjectAuthorizationError, OrganizationAuthorizationError +from annofabcli.common.exceptions import OrganizationAuthorizationError, ProjectAuthorizationError from annofabcli.common.facade import AnnofabApiFacade from annofabcli.common.typing import InputDataSize @@ -362,7 +362,8 @@ def process_common_args(self, args: argparse.Namespace): logger.info(f"args: {args}") - def validate_project(self, project_id, project_member_roles: Optional[List[ProjectMemberRole]] = None, organization_member_roles: Optional[List[OrganizationMemberRole]] = None): + def validate_project(self, project_id, project_member_roles: Optional[List[ProjectMemberRole]] = None, + organization_member_roles: Optional[List[OrganizationMemberRole]] = None): """ プロジェクト or 組織に対して、必要な権限が付与されているかを確認する。 Args: @@ -382,7 +383,7 @@ def validate_project(self, project_id, project_member_roles: Optional[List[Proje raise ProjectAuthorizationError(project_title, project_member_roles) if organization_member_roles is not None: - organization_name= self.facade.get_organization_name_from_project_id(project_id) + organization_name = self.facade.get_organization_name_from_project_id(project_id) if not self.facade.contains_any_organization_member_role(organization_name, organization_member_roles): raise OrganizationAuthorizationError(organization_name, organization_member_roles) diff --git a/annofabcli/common/exceptions.py b/annofabcli/common/exceptions.py index d8f30cd1..0afe7624 100644 --- a/annofabcli/common/exceptions.py +++ b/annofabcli/common/exceptions.py @@ -6,7 +6,7 @@ from typing import List, Optional # pylint: disable=unused-import -from annofabapi.models import ProjectMemberRole, OrganizationMemberRole +from annofabapi.models import OrganizationMemberRole, ProjectMemberRole class AnnofabCliException(Exception): @@ -27,6 +27,7 @@ def __init__(self, loing_user_id: str): class AuthorizationError(AnnofabCliException): pass + class ProjectAuthorizationError(AuthorizationError): """ AnnoFabプロジェクトに関する認可エラー @@ -36,6 +37,7 @@ def __init__(self, project_title: str, roles: List[ProjectMemberRole]): msg = f"プロジェクト: {project_title} に、ロール: {role_values} のいずれかが付与されていません。" super().__init__(msg) + class OrganizationAuthorizationError(AuthorizationError): """ AnnoFab組織に関する認可エラー diff --git a/annofabcli/common/facade.py b/annofabcli/common/facade.py index 4601acd6..cc00f5a0 100644 --- a/annofabcli/common/facade.py +++ b/annofabcli/common/facade.py @@ -9,7 +9,7 @@ import annofabapi import annofabapi.utils import more_itertools -from annofabapi.models import OrganizationMember, ProjectId, ProjectMemberRole, OrganizationMemberRole +from annofabapi.models import OrganizationMember, OrganizationMemberRole, ProjectId, ProjectMemberRole logger = logging.getLogger(__name__) @@ -209,7 +209,8 @@ def contains_any_project_member_role(self, project_id: str, roles: List[ProjectM my_role = ProjectMemberRole(my_member["member_role"]) return my_role in roles - def contains_any_organization_member_role(self, organization_name: str, roles: List[OrganizationMemberRole]) -> bool: + def contains_any_organization_member_role(self, organization_name: str, + roles: List[OrganizationMemberRole]) -> bool: """ 自分自身の組織メンバとしてのロールが、指定されたロールのいずれかに合致するかどうか Args: diff --git a/annofabcli/project/copy_project.py b/annofabcli/project/copy_project.py index 30c06cdc..1cbbd43b 100644 --- a/annofabcli/project/copy_project.py +++ b/annofabcli/project/copy_project.py @@ -1,17 +1,15 @@ import argparse +import copy import logging -from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import import uuid -import annofabapi -from annofabapi.models import OrganizationMember, ProjectMember, ProjectMemberRole, OrganizationMemberRole +from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import + +from annofabapi.models import OrganizationMemberRole, ProjectMemberRole import annofabcli -import copy from annofabcli import AnnofabApiFacade -from annofabcli.common.cli import AbstractCommandLineInterface, build_annofabapi_resource_and_login from annofabcli.common.cli import AbstractCommandLineInterface, ArgumentParser, build_annofabapi_resource_and_login - logger = logging.getLogger(__name__) @@ -19,53 +17,71 @@ class CopyProject(AbstractCommandLineInterface): """ プロジェクトをコピーする """ - @staticmethod - def find_member(members: List[Dict[str, Any]], account_id: str) -> Optional[Dict[str, Any]]: - for m in members: - if m["account_id"] == account_id: - return m - - return None - def copy_project(self, src_project_id: str, dest_project_id: str, dest_title: str, - dest_overview: Optional[str] = None, copy_options: Optional[Dict[str, bool]] = None, wait_for_completion:bool = False): + dest_overview: Optional[str] = None, copy_options: Optional[Dict[str, bool]] = None, + wait_for_completion: bool = False): """ プロジェクトメンバを、別のプロジェクトにコピーする。 Args: src_project_id: コピー元のproject_id - dest_project_id: コピー先のproject_id - delete_dest: Trueならばコピー先にしか存在しないプロジェクトメンバを削除する。 - + dest_project_id: 新しいプロジェクトのproject_id + dest_title: 新しいプロジェクトのタイトル + dest_overview: 新しいプロジェクトの概要 + copy_options: 各項目についてコピーするかどうかのオプション + wait_for_completion: プロジェクトのコピーが完了するまで待つかかどうか """ - self.validate_project(src_project_id, project_member_roles=[ProjectMemberRole.OWNER], organization_member_roles=[OrganizationMemberRole.ADMINISTRATOR, OrganizationMemberRole.OWNER]) + self.validate_project( + src_project_id, project_member_roles=[ProjectMemberRole.OWNER], + organization_member_roles=[OrganizationMemberRole.ADMINISTRATOR, OrganizationMemberRole.OWNER]) src_project_title = self.facade.get_project_title(src_project_id) + if copy_options is not None: + copy_target = [key.replace("copy_", "") for key in copy_options.keys() if copy_options[key]] + logger.info(f"コピー対象: {str(copy_target)}") + confirm_message = f"{src_project_title} ({src_project_id} を、{dest_title} ({dest_project_id}) にコピーしますか?" if not self.confirm_processing(confirm_message): return + request_body: Dict[str, Any] = {} if copy_options is not None: - query_params: Dict[str, Any] = copy.deepcopy(copy_options) - else: - query_params: Dict[str, Any] = {} + request_body = copy.deepcopy(copy_options) - query_params.update({ + request_body.update({ "dest_project_id": dest_project_id, "dest_title": dest_title, "dest_overview": dest_overview }) - self.service.api.initiate_project_copy(src_project_id, query_params=query_params) + self.service.api.initiate_project_copy(src_project_id, request_body=request_body) logger.info(f"プロジェクトのコピーを実施しています。") + if wait_for_completion: + result = self.service.wrapper.wait_for_completion(src_project_id, job_type="copy-project", + job_access_interval=60, max_job_access=15) + if result: + logger.info(f"プロジェクトのコピーが完了しました。") + else: + logger.info(f"プロジェクトのコピーは実行中 または 失敗しました。") def main(self): args = self.args - dest_project_id = args.dest_project_id if args.dest_project_id is not None else uuid.uuid4() - self.copy_project(args.src_project_id, args.dest_project_id, delete_dest=args.delete_dest) + dest_project_id = args.dest_project_id if args.dest_project_id is not None else str(uuid.uuid4()) + + copy_option_kyes = [ + "copy_inputs", "copy_tasks", "copy_annotations", "copy_webhooks", "copy_supplementaly_data", + "copy_instructions" + ] + copy_options: Dict[str, bool] = {} + for key in copy_option_kyes: + copy_options[key] = getattr(args, key) + + self.copy_project(args.project_id, dest_project_id=dest_project_id, dest_title=args.dest_title, + dest_overview=args.dest_overview, copy_options=copy_options, + wait_for_completion=args.wait_for_completion) def main(args): @@ -79,9 +95,7 @@ def parse_args(parser: argparse.ArgumentParser): argument_parser.add_project_id(help_message='コピー元のプロジェクトのproject_idを指定してください。') - - parser.add_argument('--dest_project_id', type=str, - help='新しいプロジェクトのproject_idを指定してください。省略した場合は UUIDv4 フォーマットになります。') + parser.add_argument('--dest_project_id', type=str, help='新しいプロジェクトのproject_idを指定してください。省略した場合は UUIDv4 フォーマットになります。') parser.add_argument('--dest_title', type=str, required=True, help="新しいプロジェクトのタイトルを指定してください。") parser.add_argument('--dest_overview', type=str, help="新しいプロジェクトの概要を指定してください。") @@ -92,7 +106,8 @@ def parse_args(parser: argparse.ArgumentParser): parser.add_argument('--copy_supplementaly_data', action='store_true', help="「補助情報」をコピーするかどうかを指定します。") parser.add_argument('--copy_instructions', action='store_true', help="「作業ガイド」をコピーするかどうかを指定します。") - parser.add_argument('--wait_for_completion', action='store_true', help="プロジェクトのコピーが完了するまで待ちます。") + parser.add_argument('--wait_for_completion', action='store_true', help=("プロジェクトのコピーが完了するまで待ちます。" + "1分ごとにプロジェクトのコピーが完了したかを確認し、最大15分間待ちます。")) parser.set_defaults(subcommand_func=main) diff --git a/annofabcli/project/subcommand_project.py b/annofabcli/project/subcommand_project.py index f9cfa62f..070c3fa8 100644 --- a/annofabcli/project/subcommand_project.py +++ b/annofabcli/project/subcommand_project.py @@ -2,6 +2,7 @@ import annofabcli import annofabcli.common.cli +import annofabcli.project.copy_project import annofabcli.project.diff_projects import annofabcli.project.download @@ -11,6 +12,7 @@ def parse_args(parser: argparse.ArgumentParser): subparsers = parser.add_subparsers(dest='subcommand_name') # サブコマンドの定義 + annofabcli.project.copy_project.add_parser(subparsers) annofabcli.project.diff_projects.add_parser(subparsers) annofabcli.project.download.add_parser(subparsers) From e07c1ce7ee6ef8b299a240d07114a077a626950e Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Wed, 4 Sep 2019 16:50:03 +0900 Subject: [PATCH 3/5] versionup --- annofabcli/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/annofabcli/__version__.py b/annofabcli/__version__.py index 0e1a38d3..b2809757 100644 --- a/annofabcli/__version__.py +++ b/annofabcli/__version__.py @@ -1 +1 @@ -__version__ = '1.7.0' +__version__ = '1.8.0' From 93e3ac2e1905059f3a6e5f6ae05d603dc117f91a Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Wed, 4 Sep 2019 16:50:15 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=E4=B8=8D=E5=85=B7=E5=90=88=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- annofabcli/statistics/graph.py | 4 ++-- annofabcli/statistics/visualize_statistics.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/annofabcli/statistics/graph.py b/annofabcli/statistics/graph.py index 26ff8ba4..8f35e6c8 100644 --- a/annofabcli/statistics/graph.py +++ b/annofabcli/statistics/graph.py @@ -210,8 +210,8 @@ def write_アノテーションあたり作業時間(self, task_df: pd.DataFrame title="アノテーション数と中間検査作業時間の累積グラフ", x_axis_label="アノテーション数", y_axis_label="中間検査作業時間[hour]"), dict(x="cumulative_annotation_count", y="cumulative_acceptance_worktime_hour", title="アノテーション数と受入作業時間の累積グラフ", x_axis_label="アノテーション数", y_axis_label="受入作業時間[hour]"), - dict(x="cumulative_annotation_count", y="cumulative_inspection_count", - title="アノテーション数と検査コメント数の累積グラフ", x_axis_label="アノテーション数", y_axis_label="検査コメント数"), + dict(x="cumulative_annotation_count", y="cumulative_inspection_count", title="アノテーション数と検査コメント数の累積グラフ", + x_axis_label="アノテーション数", y_axis_label="検査コメント数"), ] figs = [] diff --git a/annofabcli/statistics/visualize_statistics.py b/annofabcli/statistics/visualize_statistics.py index cd2f4937..623e2f98 100644 --- a/annofabcli/statistics/visualize_statistics.py +++ b/annofabcli/statistics/visualize_statistics.py @@ -54,7 +54,7 @@ def visualize_statistics(self, project_id: str, work_dir: Path, output_dir: Path """ - super().validate_project(project_id, roles=[ProjectMemberRole.OWNER]) + super().validate_project(project_id, project_member_roles=[ProjectMemberRole.OWNER]) checkpoint_dir = work_dir / project_id checkpoint_dir.mkdir(exist_ok=True, parents=True) From be44d7186a2b2497562d3ebc338a45a9e5d8c905 Mon Sep 17 00:00:00 2001 From: yuji38kwmt Date: Wed, 4 Sep 2019 16:58:29 +0900 Subject: [PATCH 5/5] update README.md --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 932448d8..f917ac86 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ $ docker run -it -e ANNOFAB_USER_ID=XXXX -e ANNOFAB_PASSWORD=YYYYY annofab-cli a |inspection_comment| list | 検査コメントを出力する。 |-| |inspection_comment| list_unprocessed | 未処置の検査コメントを出力する。 |-| |instruction| upload | HTMLファイルを作業ガイドとして登録する。 |チェッカー/オーナ| +|project| copy | プロジェクトをコピーする |オーナ and 組織管理者/組織オーナ| |project| diff | プロジェクト間の差分を表示する |チェッカー/オーナ| |project| download | タスクや検査コメント、アノテーションなどをダウンロードします。 |オーナ| |project_member| list | プロジェクトメンバ一覧を出力する |-| @@ -381,6 +382,27 @@ $ annofabcli instruction upload --project_id prj1 --html instruction.html +### project cooy +プロジェクトをコピーして(アノテーション仕様やメンバーを引き継いで)、新しいプロジェクトを作成します。 + + + +``` +# prj1 プロジェクトをコピーして、"prj2-title"というプロジェクトを作成する +$ annofabcli project copy --project_id prj1 --dest_title "prj2-title" + + +# prj1 プロジェクトをコピーして、"prj2"というプロジェクトIDのプロジェクトを作成する。 +# コピーが完了するまで待つ(処理を継続する) +$ annofabcli project copy --project_id prj1 --dest_title "prj2-title" --dest_project_id prj2 \ + --wait_for_completion + + +# prj1 プロジェクトの入力データと、タスクをコピーして、"prj2-title"というプロジェクトを作成する +$ annofabcli project copy --project_id prj1 --dest_title "prj2-title" --copy_inputs --copy_tasks + + +```