Skip to content

Commit

Permalink
Merge pull request #57 from kurusugawa-computer/feature/copy-project
Browse files Browse the repository at this point in the history
[project copy]プロジェクトをコピーするコマンドを作成
  • Loading branch information
yuji38kwmt authored Sep 4, 2019
2 parents 81eb8a7 + be44d71 commit 6a791ce
Show file tree
Hide file tree
Showing 15 changed files with 219 additions and 31 deletions.
18 changes: 9 additions & 9 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | プロジェクトメンバ一覧を出力する |-|
Expand Down Expand Up @@ -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


```
Expand Down
2 changes: 1 addition & 1 deletion annofabcli/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.7.0'
__version__ = '1.8.0'
2 changes: 1 addition & 1 deletion annofabcli/annotation/list_annotation_count.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
23 changes: 15 additions & 8 deletions annofabcli/common/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 OrganizationMemberRole, ProjectMemberRole

import annofabcli
from annofabcli.common.enums import FormatArgument
from annofabcli.common.exceptions import AuthorizationError
from annofabcli.common.exceptions import OrganizationAuthorizationError, ProjectAuthorizationError
from annofabcli.common.facade import AnnofabApiFacade
from annofabcli.common.typing import InputDataSize

Expand Down Expand Up @@ -362,12 +362,14 @@ 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が発生する。
Expand All @@ -376,9 +378,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:
"""
Expand Down
18 changes: 16 additions & 2 deletions annofabcli/common/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from typing import List, Optional # pylint: disable=unused-import

from annofabapi.models import ProjectMemberRole
from annofabapi.models import OrganizationMemberRole, ProjectMemberRole


class AnnofabCliException(Exception):
Expand All @@ -25,10 +25,24 @@ 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)
25 changes: 23 additions & 2 deletions annofabcli/common/facade.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import annofabapi
import annofabapi.utils
import more_itertools
from annofabapi.models import OrganizationMember, ProjectId, ProjectMemberRole
from annofabapi.models import OrganizationMember, OrganizationMemberRole, ProjectId, ProjectMemberRole

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -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:
Expand All @@ -209,6 +209,23 @@ 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:
Expand All @@ -222,6 +239,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":
Expand Down
2 changes: 1 addition & 1 deletion annofabcli/input_data/list_input_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
122 changes: 122 additions & 0 deletions annofabcli/project/copy_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import argparse
import copy
import logging
import uuid
from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import

from annofabapi.models import OrganizationMemberRole, ProjectMemberRole

import annofabcli
from annofabcli import AnnofabApiFacade
from annofabcli.common.cli import AbstractCommandLineInterface, ArgumentParser, build_annofabapi_resource_and_login

logger = logging.getLogger(__name__)


class CopyProject(AbstractCommandLineInterface):
"""
プロジェクトをコピーする
"""
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
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])

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:
request_body = copy.deepcopy(copy_options)

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, 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 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):
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=("プロジェクトのコピーが完了するまで待ちます。"
"1分ごとにプロジェクトのコピーが完了したかを確認し、最大15分間待ちます。"))

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)
2 changes: 2 additions & 0 deletions annofabcli/project/subcommand_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import annofabcli
import annofabcli.common.cli
import annofabcli.project.copy_project
import annofabcli.project.diff_projects
import annofabcli.project.download

Expand All @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions annofabcli/project_member/copy_project_members.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 6a791ce

Please sign in to comment.