annofabcli 1.108.0__py3-none-any.whl → 1.109.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- annofabcli/project/create_project.py +151 -0
- annofabcli/project/put_project.py +14 -129
- annofabcli/project/subcommand_project.py +4 -0
- annofabcli/project/update_project.py +298 -0
- {annofabcli-1.108.0.dist-info → annofabcli-1.109.0.dist-info}/METADATA +1 -1
- {annofabcli-1.108.0.dist-info → annofabcli-1.109.0.dist-info}/RECORD +9 -7
- {annofabcli-1.108.0.dist-info → annofabcli-1.109.0.dist-info}/WHEEL +0 -0
- {annofabcli-1.108.0.dist-info → annofabcli-1.109.0.dist-info}/entry_points.txt +0 -0
- {annofabcli-1.108.0.dist-info → annofabcli-1.109.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
import uuid
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
from annofabapi.models import InputDataType
|
|
11
|
+
from annofabapi.plugin import EditorPluginId, ExtendSpecsPluginId
|
|
12
|
+
|
|
13
|
+
import annofabcli
|
|
14
|
+
from annofabcli.common.cli import (
|
|
15
|
+
COMMAND_LINE_ERROR_STATUS_CODE,
|
|
16
|
+
CommandLine,
|
|
17
|
+
build_annofabapi_resource_and_login,
|
|
18
|
+
get_json_from_args,
|
|
19
|
+
)
|
|
20
|
+
from annofabcli.common.facade import AnnofabApiFacade
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CustomProjectType(Enum):
|
|
26
|
+
"""
|
|
27
|
+
カスタムプロジェクトの種類
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
THREE_DIMENSION = "3d"
|
|
31
|
+
"""3次元データ"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CreateProject(CommandLine):
|
|
35
|
+
def create_project( # noqa: ANN201
|
|
36
|
+
self,
|
|
37
|
+
organization: str,
|
|
38
|
+
title: str,
|
|
39
|
+
input_data_type: InputDataType,
|
|
40
|
+
*,
|
|
41
|
+
project_id: Optional[str],
|
|
42
|
+
overview: Optional[str],
|
|
43
|
+
editor_plugin_id: Optional[str],
|
|
44
|
+
custom_project_type: Optional[CustomProjectType],
|
|
45
|
+
configuration: Optional[dict[str, Any]],
|
|
46
|
+
):
|
|
47
|
+
new_project_id = project_id if project_id is not None else str(uuid.uuid4())
|
|
48
|
+
if configuration is None:
|
|
49
|
+
configuration = {}
|
|
50
|
+
|
|
51
|
+
if input_data_type == InputDataType.CUSTOM and custom_project_type is not None:
|
|
52
|
+
assert editor_plugin_id is None
|
|
53
|
+
editor_plugin_id = EditorPluginId.THREE_DIMENSION.value
|
|
54
|
+
configuration.update({"extended_specs_plugin_id": ExtendSpecsPluginId.THREE_DIMENSION.value})
|
|
55
|
+
|
|
56
|
+
configuration.update({"plugin_id": editor_plugin_id})
|
|
57
|
+
|
|
58
|
+
request_body = {
|
|
59
|
+
"title": title,
|
|
60
|
+
"organization_name": organization,
|
|
61
|
+
"input_data_type": input_data_type.value,
|
|
62
|
+
"overview": overview,
|
|
63
|
+
"status": "active",
|
|
64
|
+
"configuration": configuration,
|
|
65
|
+
}
|
|
66
|
+
new_project, _ = self.service.api.put_project(new_project_id, request_body=request_body)
|
|
67
|
+
logger.info(
|
|
68
|
+
f"'{organization}'組織に、project_id='{new_project['project_id']}'のプロジェクトを作成しました。 :: title='{new_project['title']}', input_data_type='{new_project['input_data_type']}'"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
COMMON_MESSAGE = "annofabcli project create: error:"
|
|
72
|
+
|
|
73
|
+
def validate(self, args: argparse.Namespace) -> bool:
|
|
74
|
+
if args.input_data_type == InputDataType.CUSTOM.value: # noqa: SIM102
|
|
75
|
+
if args.plugin_id is None and args.custom_project_type is None:
|
|
76
|
+
print( # noqa: T201
|
|
77
|
+
f"{self.COMMON_MESSAGE} '--input_data_type custom' を指定した場合は、'--plugin_id' または '--custom_project_type' が必須です。",
|
|
78
|
+
file=sys.stderr,
|
|
79
|
+
)
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
def main(self) -> None:
|
|
85
|
+
args = self.args
|
|
86
|
+
if not self.validate(args):
|
|
87
|
+
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
88
|
+
|
|
89
|
+
self.create_project(
|
|
90
|
+
args.organization,
|
|
91
|
+
args.title,
|
|
92
|
+
InputDataType(args.input_data_type),
|
|
93
|
+
project_id=args.project_id,
|
|
94
|
+
overview=args.overview,
|
|
95
|
+
editor_plugin_id=args.plugin_id,
|
|
96
|
+
custom_project_type=CustomProjectType(args.custom_project_type) if args.custom_project_type is not None else None,
|
|
97
|
+
configuration=get_json_from_args(args.configuration),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def main(args: argparse.Namespace) -> None:
|
|
102
|
+
service = build_annofabapi_resource_and_login(args)
|
|
103
|
+
facade = AnnofabApiFacade(service)
|
|
104
|
+
CreateProject(service, facade, args).main()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
108
|
+
parser.add_argument("-org", "--organization", type=str, required=True, help="プロジェクトの所属先組織")
|
|
109
|
+
|
|
110
|
+
parser.add_argument("--title", type=str, required=True, help="作成するプロジェクトのタイトル")
|
|
111
|
+
parser.add_argument(
|
|
112
|
+
"--input_data_type",
|
|
113
|
+
type=str,
|
|
114
|
+
choices=[e.value for e in InputDataType],
|
|
115
|
+
required=True,
|
|
116
|
+
help=f"プロジェクトに登録する入力データの種類\n\n * {InputDataType.IMAGE.value} : 画像\n * {InputDataType.MOVIE.value} : 動画\n * {InputDataType.CUSTOM.value} : カスタム(点群など)",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
parser.add_argument("-p", "--project_id", type=str, required=False, help="作成するプロジェクトのproject_id。未指定の場合はUUIDv4になります。")
|
|
120
|
+
parser.add_argument("--overview", type=str, help="作成するプロジェクトの概要")
|
|
121
|
+
|
|
122
|
+
group = parser.add_mutually_exclusive_group()
|
|
123
|
+
group.add_argument("--plugin_id", type=str, help="アノテーションエディタプラグインのplugin_id")
|
|
124
|
+
group.add_argument(
|
|
125
|
+
"--custom_project_type",
|
|
126
|
+
type=str,
|
|
127
|
+
choices=[e.value for e in CustomProjectType],
|
|
128
|
+
help="カスタムプロジェクトの種類。 ``--input_data_type custom`` を指定したときのみ有効です。"
|
|
129
|
+
"指定した値に対応するエディタプラグインが適用されるため、 `--plugin_id`` と同時には指定できません。\n"
|
|
130
|
+
" * 3d : 3次元データ",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
parser.add_argument(
|
|
134
|
+
"--configuration",
|
|
135
|
+
type=str,
|
|
136
|
+
help="プロジェクトの設定情報。JSON形式で指定します。"
|
|
137
|
+
"JSONの構造については https://annofab.com/docs/api/#operation/putProject のリクエストボディを参照してください。\n"
|
|
138
|
+
"``file://`` を先頭に付けると、JSON形式のファイルを指定できます。",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
parser.set_defaults(subcommand_func=main)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
|
|
145
|
+
subcommand_name = "create"
|
|
146
|
+
subcommand_help = "プロジェクトを作成します。"
|
|
147
|
+
epilog = "組織管理者、組織オーナを持つユーザで実行してください。"
|
|
148
|
+
|
|
149
|
+
parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, epilog=epilog)
|
|
150
|
+
parse_args(parser)
|
|
151
|
+
return parser
|
|
@@ -1,151 +1,36 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
-
import logging
|
|
5
4
|
import sys
|
|
6
|
-
import
|
|
7
|
-
from
|
|
8
|
-
from typing import Any, Optional
|
|
9
|
-
|
|
10
|
-
from annofabapi.models import InputDataType
|
|
11
|
-
from annofabapi.plugin import EditorPluginId, ExtendSpecsPluginId
|
|
5
|
+
from logging import getLogger
|
|
6
|
+
from typing import Optional
|
|
12
7
|
|
|
13
8
|
import annofabcli
|
|
14
|
-
from annofabcli.
|
|
15
|
-
COMMAND_LINE_ERROR_STATUS_CODE,
|
|
16
|
-
CommandLine,
|
|
17
|
-
build_annofabapi_resource_and_login,
|
|
18
|
-
get_json_from_args,
|
|
19
|
-
)
|
|
20
|
-
from annofabcli.common.facade import AnnofabApiFacade
|
|
21
|
-
|
|
22
|
-
logger = logging.getLogger(__name__)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class CustomProjectType(Enum):
|
|
26
|
-
"""
|
|
27
|
-
カスタムプロジェクトの種類
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
THREE_DIMENSION = "3d"
|
|
31
|
-
"""3次元データ"""
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class PutProject(CommandLine):
|
|
35
|
-
def put_project( # noqa: ANN201
|
|
36
|
-
self,
|
|
37
|
-
organization: str,
|
|
38
|
-
title: str,
|
|
39
|
-
input_data_type: InputDataType,
|
|
40
|
-
*,
|
|
41
|
-
project_id: Optional[str],
|
|
42
|
-
overview: Optional[str],
|
|
43
|
-
editor_plugin_id: Optional[str],
|
|
44
|
-
custom_project_type: Optional[CustomProjectType],
|
|
45
|
-
configuration: Optional[dict[str, Any]],
|
|
46
|
-
):
|
|
47
|
-
new_project_id = project_id if project_id is not None else str(uuid.uuid4())
|
|
48
|
-
if configuration is None:
|
|
49
|
-
configuration = {}
|
|
50
|
-
|
|
51
|
-
if input_data_type == InputDataType.CUSTOM and custom_project_type is not None:
|
|
52
|
-
assert editor_plugin_id is None
|
|
53
|
-
editor_plugin_id = EditorPluginId.THREE_DIMENSION.value
|
|
54
|
-
configuration.update({"extended_specs_plugin_id": ExtendSpecsPluginId.THREE_DIMENSION.value})
|
|
55
|
-
|
|
56
|
-
configuration.update({"plugin_id": editor_plugin_id})
|
|
57
|
-
|
|
58
|
-
request_body = {
|
|
59
|
-
"title": title,
|
|
60
|
-
"organization_name": organization,
|
|
61
|
-
"input_data_type": input_data_type.value,
|
|
62
|
-
"overview": overview,
|
|
63
|
-
"status": "active",
|
|
64
|
-
"configuration": configuration,
|
|
65
|
-
}
|
|
66
|
-
new_project, _ = self.service.api.put_project(new_project_id, request_body=request_body)
|
|
67
|
-
logger.info(
|
|
68
|
-
f"'{organization}'組織に、project_id='{new_project['project_id']}'のプロジェクトを作成しました。 :: title='{new_project['title']}', input_data_type='{new_project['input_data_type']}'"
|
|
69
|
-
)
|
|
9
|
+
from annofabcli.project import create_project
|
|
70
10
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def validate(self, args: argparse.Namespace) -> bool:
|
|
74
|
-
if args.input_data_type == InputDataType.CUSTOM.value: # noqa: SIM102
|
|
75
|
-
if args.plugin_id is None and args.custom_project_type is None:
|
|
76
|
-
print( # noqa: T201
|
|
77
|
-
f"{self.COMMON_MESSAGE} '--input_data_type custom' を指定した場合は、'--plugin_id' または '--custom_project_type' が必須です。",
|
|
78
|
-
file=sys.stderr,
|
|
79
|
-
)
|
|
80
|
-
return False
|
|
81
|
-
|
|
82
|
-
return True
|
|
83
|
-
|
|
84
|
-
def main(self) -> None:
|
|
85
|
-
args = self.args
|
|
86
|
-
if not self.validate(args):
|
|
87
|
-
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
88
|
-
|
|
89
|
-
self.put_project(
|
|
90
|
-
args.organization,
|
|
91
|
-
args.title,
|
|
92
|
-
InputDataType(args.input_data_type),
|
|
93
|
-
project_id=args.project_id,
|
|
94
|
-
overview=args.overview,
|
|
95
|
-
editor_plugin_id=args.plugin_id,
|
|
96
|
-
custom_project_type=CustomProjectType(args.custom_project_type) if args.custom_project_type is not None else None,
|
|
97
|
-
configuration=get_json_from_args(args.configuration),
|
|
98
|
-
)
|
|
11
|
+
logger = getLogger(__name__)
|
|
99
12
|
|
|
100
13
|
|
|
101
14
|
def main(args: argparse.Namespace) -> None:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
15
|
+
print("[DEPRECATED] :: `project put` コマンドは非推奨です。代わりに `project create` コマンドを使用してください。`project put` コマンドは2026年01月01日以降に廃止予定です。", file=sys.stderr) # noqa: T201
|
|
16
|
+
# create_project.py の実装を使用
|
|
17
|
+
create_project.main(args)
|
|
105
18
|
|
|
106
19
|
|
|
107
20
|
def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
parser.add_argument(
|
|
112
|
-
"--input_data_type",
|
|
113
|
-
type=str,
|
|
114
|
-
choices=[e.value for e in InputDataType],
|
|
115
|
-
required=True,
|
|
116
|
-
help=f"プロジェクトに登録する入力データの種類\n\n * {InputDataType.IMAGE.value} : 画像\n * {InputDataType.MOVIE.value} : 動画\n * {InputDataType.CUSTOM.value} : カスタム(点群など)",
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
parser.add_argument("-p", "--project_id", type=str, required=False, help="作成するプロジェクトのproject_id。未指定の場合はUUIDv4になります。")
|
|
120
|
-
parser.add_argument("--overview", type=str, help="作成するプロジェクトの概要")
|
|
121
|
-
|
|
122
|
-
group = parser.add_mutually_exclusive_group()
|
|
123
|
-
group.add_argument("--plugin_id", type=str, help="アノテーションエディタプラグインのplugin_id")
|
|
124
|
-
group.add_argument(
|
|
125
|
-
"--custom_project_type",
|
|
126
|
-
type=str,
|
|
127
|
-
choices=[e.value for e in CustomProjectType],
|
|
128
|
-
help="カスタムプロジェクトの種類。 ``--input_data_type custom`` を指定したときのみ有効です。"
|
|
129
|
-
"指定した値に対応するエディタプラグインが適用されるため、 `--plugin_id`` と同時には指定できません。\n"
|
|
130
|
-
" * 3d : 3次元データ",
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
parser.add_argument(
|
|
134
|
-
"--configuration",
|
|
135
|
-
type=str,
|
|
136
|
-
help="プロジェクトの設定情報。JSON形式で指定します。"
|
|
137
|
-
"JSONの構造については https://annofab.com/docs/api/#operation/putProject のリクエストボディを参照してください。\n"
|
|
138
|
-
"``file://`` を先頭に付けると、JSON形式のファイルを指定できます。",
|
|
139
|
-
)
|
|
140
|
-
|
|
21
|
+
# create_project.py のparse_argsと同じ実装を使用
|
|
22
|
+
create_project.parse_args(parser)
|
|
23
|
+
# main関数のみ差し替え
|
|
141
24
|
parser.set_defaults(subcommand_func=main)
|
|
142
25
|
|
|
143
26
|
|
|
144
27
|
def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
|
|
145
28
|
subcommand_name = "put"
|
|
146
|
-
subcommand_help = "プロジェクトを作成します。"
|
|
29
|
+
subcommand_help = "[DEPRECATED] プロジェクトを作成します。"
|
|
30
|
+
subcommand_description = subcommand_help + "\n`project put` コマンドは非推奨です。代わりに 'project create'コマンドを使用してください。`project put` コマンドは2026年01月01日以降に廃止予定です。"
|
|
31
|
+
|
|
147
32
|
epilog = "組織管理者、組織オーナを持つユーザで実行してください。"
|
|
148
33
|
|
|
149
|
-
parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, epilog=epilog)
|
|
34
|
+
parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description=subcommand_description, epilog=epilog)
|
|
150
35
|
parse_args(parser)
|
|
151
36
|
return parser
|
|
@@ -4,10 +4,12 @@ from typing import Optional
|
|
|
4
4
|
import annofabcli.project.change_organization_of_project
|
|
5
5
|
import annofabcli.project.change_project_status
|
|
6
6
|
import annofabcli.project.copy_project
|
|
7
|
+
import annofabcli.project.create_project
|
|
7
8
|
import annofabcli.project.diff_projects
|
|
8
9
|
import annofabcli.project.list_project
|
|
9
10
|
import annofabcli.project.put_project
|
|
10
11
|
import annofabcli.project.update_configuration
|
|
12
|
+
import annofabcli.project.update_project
|
|
11
13
|
from annofabcli.common.cli import add_parser as common_add_parser
|
|
12
14
|
|
|
13
15
|
|
|
@@ -18,10 +20,12 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
18
20
|
annofabcli.project.change_organization_of_project.add_parser(subparsers)
|
|
19
21
|
annofabcli.project.change_project_status.add_parser(subparsers)
|
|
20
22
|
annofabcli.project.copy_project.add_parser(subparsers)
|
|
23
|
+
annofabcli.project.create_project.add_parser(subparsers)
|
|
21
24
|
annofabcli.project.diff_projects.add_parser(subparsers)
|
|
22
25
|
annofabcli.project.list_project.add_parser(subparsers)
|
|
23
26
|
annofabcli.project.put_project.add_parser(subparsers)
|
|
24
27
|
annofabcli.project.update_configuration.add_parser(subparsers)
|
|
28
|
+
annofabcli.project.update_project.add_parser(subparsers)
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import copy
|
|
5
|
+
import enum
|
|
6
|
+
import logging
|
|
7
|
+
import multiprocessing
|
|
8
|
+
import sys
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from functools import partial
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
import annofabapi
|
|
15
|
+
import pandas
|
|
16
|
+
from pydantic import BaseModel
|
|
17
|
+
|
|
18
|
+
import annofabcli
|
|
19
|
+
import annofabcli.common.cli
|
|
20
|
+
from annofabcli.common.cli import (
|
|
21
|
+
COMMAND_LINE_ERROR_STATUS_CODE,
|
|
22
|
+
PARALLELISM_CHOICES,
|
|
23
|
+
CommandLine,
|
|
24
|
+
CommandLineWithConfirm,
|
|
25
|
+
build_annofabapi_resource_and_login,
|
|
26
|
+
get_json_from_args,
|
|
27
|
+
)
|
|
28
|
+
from annofabcli.common.facade import AnnofabApiFacade
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class UpdateResult(Enum):
|
|
34
|
+
"""更新結果の種類"""
|
|
35
|
+
|
|
36
|
+
SUCCESS = enum.auto()
|
|
37
|
+
"""更新に成功した"""
|
|
38
|
+
SKIPPED = enum.auto()
|
|
39
|
+
"""更新を実行しなかった(存在しないproject_id、ユーザー拒否等)"""
|
|
40
|
+
FAILED = enum.auto()
|
|
41
|
+
"""更新を試みたが例外で失敗"""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class UpdatedProject(BaseModel):
|
|
45
|
+
"""
|
|
46
|
+
更新されるプロジェクト
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
project_id: str
|
|
50
|
+
"""更新対象のプロジェクトを表すID"""
|
|
51
|
+
title: Optional[str] = None
|
|
52
|
+
"""変更後のプロジェクトタイトル(指定した場合のみ更新)"""
|
|
53
|
+
overview: Optional[str] = None
|
|
54
|
+
"""変更後のプロジェクト概要(指定した場合のみ更新)"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class UpdateProjectMain(CommandLineWithConfirm):
|
|
58
|
+
def __init__(self, service: annofabapi.Resource, *, all_yes: bool = False) -> None:
|
|
59
|
+
self.service = service
|
|
60
|
+
CommandLineWithConfirm.__init__(self, all_yes)
|
|
61
|
+
|
|
62
|
+
def update_project(
|
|
63
|
+
self,
|
|
64
|
+
project_id: str,
|
|
65
|
+
*,
|
|
66
|
+
new_title: Optional[str] = None,
|
|
67
|
+
new_overview: Optional[str] = None,
|
|
68
|
+
project_index: Optional[int] = None,
|
|
69
|
+
) -> UpdateResult:
|
|
70
|
+
"""
|
|
71
|
+
1個のプロジェクトを更新します。
|
|
72
|
+
"""
|
|
73
|
+
# ログメッセージの先頭の変数
|
|
74
|
+
log_prefix = f"project_id='{project_id}' :: "
|
|
75
|
+
if project_index is not None:
|
|
76
|
+
log_prefix = f"{project_index + 1}件目 :: {log_prefix}"
|
|
77
|
+
|
|
78
|
+
old_project = self.service.wrapper.get_project_or_none(project_id)
|
|
79
|
+
if old_project is None:
|
|
80
|
+
logger.warning(f"{log_prefix}プロジェクトは存在しません。")
|
|
81
|
+
return UpdateResult.SKIPPED
|
|
82
|
+
|
|
83
|
+
# 更新する内容の確認メッセージを作成
|
|
84
|
+
changes = []
|
|
85
|
+
if new_title is not None:
|
|
86
|
+
changes.append(f"title='{old_project['title']}'を'{new_title}'に変更")
|
|
87
|
+
if new_overview is not None:
|
|
88
|
+
changes.append(f"overview='{old_project['overview']}'を'{new_overview}'に変更")
|
|
89
|
+
|
|
90
|
+
if len(changes) == 0:
|
|
91
|
+
logger.warning(f"{log_prefix}更新する内容が指定されていません。")
|
|
92
|
+
return UpdateResult.SKIPPED
|
|
93
|
+
|
|
94
|
+
change_message = "、".join(changes)
|
|
95
|
+
if not self.confirm_processing(f"{log_prefix}{change_message}しますか?"):
|
|
96
|
+
return UpdateResult.SKIPPED
|
|
97
|
+
|
|
98
|
+
request_body = copy.deepcopy(old_project)
|
|
99
|
+
request_body["last_updated_datetime"] = old_project["updated_datetime"]
|
|
100
|
+
request_body["status"] = old_project["project_status"]
|
|
101
|
+
|
|
102
|
+
if new_title is not None:
|
|
103
|
+
request_body["title"] = new_title
|
|
104
|
+
if new_overview is not None:
|
|
105
|
+
request_body["overview"] = new_overview
|
|
106
|
+
|
|
107
|
+
self.service.api.put_project(project_id, request_body=request_body)
|
|
108
|
+
logger.debug(f"{log_prefix}プロジェクトを更新しました。 :: {change_message}")
|
|
109
|
+
return UpdateResult.SUCCESS
|
|
110
|
+
|
|
111
|
+
def update_project_list_sequentially(
|
|
112
|
+
self,
|
|
113
|
+
updated_project_list: list[UpdatedProject],
|
|
114
|
+
) -> None:
|
|
115
|
+
"""複数のプロジェクトを逐次的に更新します。"""
|
|
116
|
+
success_count = 0
|
|
117
|
+
skipped_count = 0 # 更新を実行しなかった個数
|
|
118
|
+
failed_count = 0 # 更新に失敗した個数
|
|
119
|
+
|
|
120
|
+
logger.info(f"{len(updated_project_list)} 件のプロジェクトを更新します。")
|
|
121
|
+
|
|
122
|
+
for project_index, updated_project in enumerate(updated_project_list):
|
|
123
|
+
current_num = project_index + 1
|
|
124
|
+
|
|
125
|
+
# 進捗ログ出力
|
|
126
|
+
if current_num % 100 == 0:
|
|
127
|
+
logger.info(f"{current_num} / {len(updated_project_list)} 件目のプロジェクトを処理中...")
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
result = self.update_project(
|
|
131
|
+
updated_project.project_id,
|
|
132
|
+
new_title=updated_project.title,
|
|
133
|
+
new_overview=updated_project.overview,
|
|
134
|
+
project_index=project_index,
|
|
135
|
+
)
|
|
136
|
+
if result == UpdateResult.SUCCESS:
|
|
137
|
+
success_count += 1
|
|
138
|
+
elif result == UpdateResult.SKIPPED:
|
|
139
|
+
skipped_count += 1
|
|
140
|
+
except Exception:
|
|
141
|
+
logger.warning(f"{current_num}件目 :: project_id='{updated_project.project_id}'のプロジェクトを更新するのに失敗しました。", exc_info=True)
|
|
142
|
+
failed_count += 1
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
logger.info(f"{success_count} / {len(updated_project_list)} 件のプロジェクトを更新しました。(成功: {success_count}件, スキップ: {skipped_count}件, 失敗: {failed_count}件)")
|
|
146
|
+
|
|
147
|
+
def _update_project_wrapper(self, args: tuple[int, UpdatedProject]) -> UpdateResult:
|
|
148
|
+
index, updated_project = args
|
|
149
|
+
try:
|
|
150
|
+
return self.update_project(
|
|
151
|
+
project_id=updated_project.project_id,
|
|
152
|
+
new_title=updated_project.title,
|
|
153
|
+
new_overview=updated_project.overview,
|
|
154
|
+
project_index=index,
|
|
155
|
+
)
|
|
156
|
+
except Exception:
|
|
157
|
+
logger.warning(f"{index + 1}件目 :: project_id='{updated_project.project_id}'のプロジェクトを更新するのに失敗しました。", exc_info=True)
|
|
158
|
+
return UpdateResult.FAILED
|
|
159
|
+
|
|
160
|
+
def update_project_list_in_parallel(
|
|
161
|
+
self,
|
|
162
|
+
updated_project_list: list[UpdatedProject],
|
|
163
|
+
parallelism: int,
|
|
164
|
+
) -> None:
|
|
165
|
+
"""複数のプロジェクトを並列的に更新します。"""
|
|
166
|
+
|
|
167
|
+
logger.info(f"{len(updated_project_list)} 件のプロジェクトを更新します。{parallelism}個のプロセスを使用して並列実行します。")
|
|
168
|
+
|
|
169
|
+
partial_func = partial(self._update_project_wrapper)
|
|
170
|
+
with multiprocessing.Pool(parallelism) as pool:
|
|
171
|
+
result_list = pool.map(partial_func, enumerate(updated_project_list))
|
|
172
|
+
success_count = len([e for e in result_list if e == UpdateResult.SUCCESS])
|
|
173
|
+
skipped_count = len([e for e in result_list if e == UpdateResult.SKIPPED])
|
|
174
|
+
failed_count = len([e for e in result_list if e == UpdateResult.FAILED])
|
|
175
|
+
|
|
176
|
+
logger.info(f"{success_count} / {len(updated_project_list)} 件のプロジェクトを更新しました。(成功: {success_count}件, スキップ: {skipped_count}件, 失敗: {failed_count}件)")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def create_updated_project_list_from_dict(project_dict_list: list[dict[str, str]]) -> list[UpdatedProject]:
|
|
180
|
+
return [UpdatedProject.model_validate(e) for e in project_dict_list]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def create_updated_project_list_from_csv(csv_file: Path) -> list[UpdatedProject]:
|
|
184
|
+
"""プロジェクトの情報が記載されているCSVを読み込み、UpdatedProjectのlistを返します。
|
|
185
|
+
CSVには以下の列が存在します。
|
|
186
|
+
* project_id (必須)
|
|
187
|
+
* title (任意)
|
|
188
|
+
* overview (任意)
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
csv_file (Path): CSVファイルのパス
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
更新対象のプロジェクトのlist
|
|
195
|
+
"""
|
|
196
|
+
df_project = pandas.read_csv(
|
|
197
|
+
csv_file,
|
|
198
|
+
# 文字列として読み込むようにする
|
|
199
|
+
dtype={"project_id": "string", "title": "string", "overview": "string"},
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
project_dict_list = df_project.to_dict("records")
|
|
203
|
+
return [UpdatedProject.model_validate(e) for e in project_dict_list]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
CLI_COMMON_MESSAGE = "annofabcli project update: error:"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class UpdateProject(CommandLine):
|
|
210
|
+
@staticmethod
|
|
211
|
+
def validate(args: argparse.Namespace) -> bool:
|
|
212
|
+
if args.parallelism is not None and not args.yes:
|
|
213
|
+
print( # noqa: T201
|
|
214
|
+
f"{CLI_COMMON_MESSAGE} argument --parallelism: '--parallelism'を指定するときは、'--yes' も指定する必要があります。",
|
|
215
|
+
file=sys.stderr,
|
|
216
|
+
)
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
return True
|
|
220
|
+
|
|
221
|
+
def main(self) -> None:
|
|
222
|
+
args = self.args
|
|
223
|
+
if not self.validate(args):
|
|
224
|
+
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
225
|
+
|
|
226
|
+
main_obj = UpdateProjectMain(self.service, all_yes=self.all_yes)
|
|
227
|
+
|
|
228
|
+
if args.csv is not None:
|
|
229
|
+
updated_project_list = create_updated_project_list_from_csv(args.csv)
|
|
230
|
+
|
|
231
|
+
elif args.json is not None:
|
|
232
|
+
project_dict_list = get_json_from_args(args.json)
|
|
233
|
+
if not isinstance(project_dict_list, list):
|
|
234
|
+
print(f"{CLI_COMMON_MESSAGE} JSON形式が不正です。オブジェクトの配列を指定してください。", file=sys.stderr) # noqa: T201
|
|
235
|
+
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
236
|
+
updated_project_list = create_updated_project_list_from_dict(project_dict_list)
|
|
237
|
+
else:
|
|
238
|
+
raise RuntimeError("argparse により相互排他が保証されているため、ここには到達しません")
|
|
239
|
+
|
|
240
|
+
if args.parallelism is not None:
|
|
241
|
+
main_obj.update_project_list_in_parallel(updated_project_list=updated_project_list, parallelism=args.parallelism)
|
|
242
|
+
else:
|
|
243
|
+
main_obj.update_project_list_sequentially(updated_project_list=updated_project_list)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def main(args: argparse.Namespace) -> None:
|
|
247
|
+
service = build_annofabapi_resource_and_login(args)
|
|
248
|
+
facade = AnnofabApiFacade(service)
|
|
249
|
+
UpdateProject(service, facade, args).main()
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
253
|
+
file_group = parser.add_mutually_exclusive_group(required=True)
|
|
254
|
+
file_group.add_argument(
|
|
255
|
+
"--csv",
|
|
256
|
+
type=Path,
|
|
257
|
+
help=(
|
|
258
|
+
"更新対象のプロジェクトと更新後の値が記載されたCSVファイルのパスを指定します。\n"
|
|
259
|
+
"CSVのフォーマットは以下の通りです。"
|
|
260
|
+
"\n"
|
|
261
|
+
" * ヘッダ行あり, カンマ区切り\n"
|
|
262
|
+
" * project_id (required)\n"
|
|
263
|
+
" * title (optional)\n"
|
|
264
|
+
" * overview (optional)\n"
|
|
265
|
+
"更新しないプロパティは、セルの値を空欄にしてください。\n"
|
|
266
|
+
),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
JSON_SAMPLE = '[{"project_id":"prj1","title":"new_title1"},{"project_id":"prj2","overview":"new_overview2"}]' # noqa: N806
|
|
270
|
+
file_group.add_argument(
|
|
271
|
+
"--json",
|
|
272
|
+
type=str,
|
|
273
|
+
help=(
|
|
274
|
+
"更新対象のプロジェクトと更新後の値をJSON形式で指定します。\n"
|
|
275
|
+
"JSONの各キーは ``--csv`` に渡すCSVの各列に対応しています。\n"
|
|
276
|
+
"``file://`` を先頭に付けるとjsonファイルを指定できます。\n"
|
|
277
|
+
f"(ex) ``{JSON_SAMPLE}`` \n"
|
|
278
|
+
"更新しないプロパティは、キーを記載しないか値をnullにしてください。\n"
|
|
279
|
+
),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
parser.add_argument(
|
|
283
|
+
"--parallelism",
|
|
284
|
+
type=int,
|
|
285
|
+
choices=PARALLELISM_CHOICES,
|
|
286
|
+
help="使用するプロセス数(並列度)。指定しない場合は、逐次的に処理します。指定する場合は ``--yes`` も一緒に指定する必要があります。",
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
parser.set_defaults(subcommand_func=main)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
|
|
293
|
+
subcommand_name = "update"
|
|
294
|
+
subcommand_help = "プロジェクトのタイトルまたは概要を更新します。"
|
|
295
|
+
epilog = "プロジェクトオーナロールを持つユーザで実行してください。"
|
|
296
|
+
parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, epilog=epilog)
|
|
297
|
+
parse_args(parser)
|
|
298
|
+
return parser
|
|
@@ -120,11 +120,13 @@ annofabcli/project/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuF
|
|
|
120
120
|
annofabcli/project/change_organization_of_project.py,sha256=yeR8Xu4nmvrA4jGfsPDmFxf-qJxvEVQAvluierJRMxc,11300
|
|
121
121
|
annofabcli/project/change_project_status.py,sha256=AQQFFYjUnwrFshuYS_xSGyLAp2lJ-7SSIuaigPTLBYk,8657
|
|
122
122
|
annofabcli/project/copy_project.py,sha256=O1TooJ0VVbRP84d4Y-8e6Z7pKMwhBEwPu3EBv15hPlY,7131
|
|
123
|
+
annofabcli/project/create_project.py,sha256=WK1mh6c_2sh-98Q_ZxYIGOZPKe22KnmJS8q_l2gNUdA,6051
|
|
123
124
|
annofabcli/project/diff_projects.py,sha256=OsR0otV6MOCbUgGEDsYganu5cuHYZbpHbBAqujwBAw8,16531
|
|
124
125
|
annofabcli/project/list_project.py,sha256=OWnbCyShI4ceugxML8kSAD2nkyld5MlpXfbvQu2__4Y,10367
|
|
125
|
-
annofabcli/project/put_project.py,sha256=
|
|
126
|
-
annofabcli/project/subcommand_project.py,sha256=
|
|
126
|
+
annofabcli/project/put_project.py,sha256=b_ZaMhYTgqC-W5q2FpT-BycnPm9QmduHRvG0cVpAK_A,1567
|
|
127
|
+
annofabcli/project/subcommand_project.py,sha256=ZfaBWUP3r7QW1avj_FBI8kyz5CtHt39v1MOii1Ju8TQ,1651
|
|
127
128
|
annofabcli/project/update_configuration.py,sha256=Gnymx6SwnLARY8oSgVLbG9q7y6WRt06i5RQpv6LTcFI,6491
|
|
129
|
+
annofabcli/project/update_project.py,sha256=OqEieEQyIoVjtJxeaaZfeKjrCl7_YXzQBzC8lCVwqN4,12089
|
|
128
130
|
annofabcli/project_member/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
129
131
|
annofabcli/project_member/change_project_members.py,sha256=T7l7--T2Mb8zLKKfGq3qB-wiAie6rkt2V-iq9e2LDp4,9337
|
|
130
132
|
annofabcli/project_member/copy_project_members.py,sha256=Rm98Ks0PE4zdoXqpu_itNLvBkh1d5x_J7CKjdTkhKLk,8839
|
|
@@ -215,8 +217,8 @@ annofabcli/task_history_event/download_task_history_event_json.py,sha256=hQLVbQ0
|
|
|
215
217
|
annofabcli/task_history_event/list_all_task_history_event.py,sha256=EeKMyPUxGwYCFtWQHHW954ZserGm8lUqrwNnV1iX9X4,6830
|
|
216
218
|
annofabcli/task_history_event/list_worktime.py,sha256=Y7Pu5DP7scPf7HPt6CTiTvB1_5_Nfi1bStUIaCpkhII,15507
|
|
217
219
|
annofabcli/task_history_event/subcommand_task_history_event.py,sha256=mJVJoT4RXk4HWnY7-Nrsl4If-gtaIIEXd2z7eFZwM2I,1260
|
|
218
|
-
annofabcli-1.
|
|
219
|
-
annofabcli-1.
|
|
220
|
-
annofabcli-1.
|
|
221
|
-
annofabcli-1.
|
|
222
|
-
annofabcli-1.
|
|
220
|
+
annofabcli-1.109.0.dist-info/METADATA,sha256=pbrL21qmMSLyM9m8jkG-ACFsgHGr9DKBVub588V7jP0,5134
|
|
221
|
+
annofabcli-1.109.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
222
|
+
annofabcli-1.109.0.dist-info/entry_points.txt,sha256=C2uSUc-kkLJpoK_mDL5FEMAdorLEMPfwSf8VBMYnIFM,56
|
|
223
|
+
annofabcli-1.109.0.dist-info/licenses/LICENSE,sha256=pcqWYfxFtxBzhvKp3x9MXNM4xciGb2eFewaRhXUNHlo,1081
|
|
224
|
+
annofabcli-1.109.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|