annofabcli 1.107.1__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/annotation/change_annotation_attributes.py +1 -1
- annofabcli/annotation/create_classification_annotation.py +408 -0
- annofabcli/annotation/delete_annotation.py +50 -14
- annofabcli/annotation/import_annotation.py +7 -8
- annofabcli/annotation/subcommand_annotation.py +2 -0
- annofabcli/input_data/subcommand_input_data.py +2 -2
- annofabcli/input_data/update_input_data.py +308 -0
- annofabcli/project/create_project.py +151 -0
- annofabcli/project/put_project.py +14 -129
- annofabcli/project/subcommand_project.py +6 -0
- annofabcli/project/update_configuration.py +151 -0
- annofabcli/project/update_project.py +298 -0
- annofabcli/statistics/list_video_duration.py +2 -2
- annofabcli/task/complete_tasks.py +20 -20
- {annofabcli-1.107.1.dist-info → annofabcli-1.109.0.dist-info}/METADATA +2 -2
- {annofabcli-1.107.1.dist-info → annofabcli-1.109.0.dist-info}/RECORD +19 -15
- annofabcli/input_data/change_input_data_name.py +0 -245
- {annofabcli-1.107.1.dist-info → annofabcli-1.109.0.dist-info}/WHEEL +0 -0
- {annofabcli-1.107.1.dist-info → annofabcli-1.109.0.dist-info}/entry_points.txt +0 -0
- {annofabcli-1.107.1.dist-info → annofabcli-1.109.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import enum
|
|
5
|
+
import logging
|
|
6
|
+
import multiprocessing
|
|
7
|
+
import sys
|
|
8
|
+
from dataclasses import dataclass
|
|
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 dataclasses_json import DataClassJsonMixin
|
|
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
|
+
ArgumentParser,
|
|
24
|
+
CommandLine,
|
|
25
|
+
CommandLineWithConfirm,
|
|
26
|
+
build_annofabapi_resource_and_login,
|
|
27
|
+
get_json_from_args,
|
|
28
|
+
)
|
|
29
|
+
from annofabcli.common.facade import AnnofabApiFacade
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class UpdateResult(Enum):
|
|
35
|
+
"""更新結果の種類"""
|
|
36
|
+
|
|
37
|
+
SUCCESS = enum.auto()
|
|
38
|
+
"""更新に成功した"""
|
|
39
|
+
SKIPPED = enum.auto()
|
|
40
|
+
"""更新を実行しなかった(存在しないinput_data_id、ユーザー拒否等)"""
|
|
41
|
+
FAILED = enum.auto()
|
|
42
|
+
"""更新を試みたが例外で失敗"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class UpdatedInputData(DataClassJsonMixin):
|
|
47
|
+
"""
|
|
48
|
+
更新される入力データ
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
input_data_id: str
|
|
52
|
+
"""更新対象の入力データを表すID"""
|
|
53
|
+
input_data_name: Optional[str] = None
|
|
54
|
+
"""変更後の入力データ名(指定した場合のみ更新)"""
|
|
55
|
+
input_data_path: Optional[str] = None
|
|
56
|
+
"""変更後の入力データパス(指定した場合のみ更新)"""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class UpdateInputDataMain(CommandLineWithConfirm):
|
|
60
|
+
def __init__(self, service: annofabapi.Resource, *, all_yes: bool = False) -> None:
|
|
61
|
+
self.service = service
|
|
62
|
+
CommandLineWithConfirm.__init__(self, all_yes)
|
|
63
|
+
|
|
64
|
+
def update_input_data(
|
|
65
|
+
self,
|
|
66
|
+
project_id: str,
|
|
67
|
+
input_data_id: str,
|
|
68
|
+
*,
|
|
69
|
+
new_input_data_name: Optional[str] = None,
|
|
70
|
+
new_input_data_path: Optional[str] = None,
|
|
71
|
+
input_data_index: Optional[int] = None,
|
|
72
|
+
) -> UpdateResult:
|
|
73
|
+
"""
|
|
74
|
+
1個の入力データを更新します。
|
|
75
|
+
"""
|
|
76
|
+
# ログメッセージの先頭の変数
|
|
77
|
+
log_prefix = f"input_data_id='{input_data_id}' :: "
|
|
78
|
+
if input_data_index is not None:
|
|
79
|
+
log_prefix = f"{input_data_index + 1}件目 :: {log_prefix}"
|
|
80
|
+
|
|
81
|
+
old_input_data = self.service.wrapper.get_input_data_or_none(project_id, input_data_id)
|
|
82
|
+
if old_input_data is None:
|
|
83
|
+
logger.warning(f"{log_prefix}入力データは存在しません。")
|
|
84
|
+
return UpdateResult.SKIPPED
|
|
85
|
+
|
|
86
|
+
# 更新する内容の確認メッセージを作成
|
|
87
|
+
changes = []
|
|
88
|
+
if new_input_data_name is not None:
|
|
89
|
+
changes.append(f"input_data_name='{old_input_data['input_data_name']}'を'{new_input_data_name}'に変更")
|
|
90
|
+
if new_input_data_path is not None:
|
|
91
|
+
changes.append(f"input_data_path='{old_input_data['input_data_path']}'を'{new_input_data_path}'に変更")
|
|
92
|
+
|
|
93
|
+
if len(changes) == 0:
|
|
94
|
+
logger.warning(f"{log_prefix}更新する内容が指定されていません。")
|
|
95
|
+
return UpdateResult.SKIPPED
|
|
96
|
+
|
|
97
|
+
change_message = "、".join(changes)
|
|
98
|
+
if not self.confirm_processing(f"{log_prefix}{change_message}しますか?"):
|
|
99
|
+
return UpdateResult.SKIPPED
|
|
100
|
+
|
|
101
|
+
request_body = old_input_data
|
|
102
|
+
request_body["last_updated_datetime"] = old_input_data["updated_datetime"]
|
|
103
|
+
|
|
104
|
+
if new_input_data_name is not None:
|
|
105
|
+
request_body["input_data_name"] = new_input_data_name
|
|
106
|
+
if new_input_data_path is not None:
|
|
107
|
+
request_body["input_data_path"] = new_input_data_path
|
|
108
|
+
|
|
109
|
+
self.service.api.put_input_data(project_id, input_data_id, request_body=request_body)
|
|
110
|
+
logger.debug(f"{log_prefix} :: 入力データを更新しました。 :: {changes}")
|
|
111
|
+
return UpdateResult.SUCCESS
|
|
112
|
+
|
|
113
|
+
def update_input_data_list_sequentially(
|
|
114
|
+
self,
|
|
115
|
+
project_id: str,
|
|
116
|
+
updated_input_data_list: list[UpdatedInputData],
|
|
117
|
+
) -> None:
|
|
118
|
+
"""複数の入力データを逐次的に更新します。"""
|
|
119
|
+
success_count = 0
|
|
120
|
+
skipped_count = 0 # 更新を実行しなかった個数
|
|
121
|
+
failed_count = 0 # 更新に失敗した個数
|
|
122
|
+
|
|
123
|
+
logger.info(f"{len(updated_input_data_list)} 件の入力データを更新します。")
|
|
124
|
+
|
|
125
|
+
for input_data_index, updated_input_data in enumerate(updated_input_data_list):
|
|
126
|
+
current_num = input_data_index + 1
|
|
127
|
+
|
|
128
|
+
# 進捗ログ出力
|
|
129
|
+
if current_num % 1000 == 0:
|
|
130
|
+
logger.info(f"{current_num} / {len(updated_input_data_list)} 件目の入力データを処理中...")
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
result = self.update_input_data(
|
|
134
|
+
project_id,
|
|
135
|
+
updated_input_data.input_data_id,
|
|
136
|
+
new_input_data_name=updated_input_data.input_data_name,
|
|
137
|
+
new_input_data_path=updated_input_data.input_data_path,
|
|
138
|
+
input_data_index=input_data_index,
|
|
139
|
+
)
|
|
140
|
+
if result == UpdateResult.SUCCESS:
|
|
141
|
+
success_count += 1
|
|
142
|
+
elif result == UpdateResult.SKIPPED:
|
|
143
|
+
skipped_count += 1
|
|
144
|
+
except Exception:
|
|
145
|
+
logger.warning(f"{current_num}件目 :: input_data_id='{updated_input_data.input_data_id}'の入力データを更新するのに失敗しました。", exc_info=True)
|
|
146
|
+
failed_count += 1
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
logger.info(f"{success_count} / {len(updated_input_data_list)} 件の入力データを更新しました。(成功: {success_count}件, スキップ: {skipped_count}件, 失敗: {failed_count}件)")
|
|
150
|
+
|
|
151
|
+
def _update_input_data_wrapper(self, args: tuple[int, UpdatedInputData], project_id: str) -> UpdateResult:
|
|
152
|
+
index, updated_input_data = args
|
|
153
|
+
try:
|
|
154
|
+
return self.update_input_data(
|
|
155
|
+
project_id,
|
|
156
|
+
input_data_id=updated_input_data.input_data_id,
|
|
157
|
+
new_input_data_name=updated_input_data.input_data_name,
|
|
158
|
+
new_input_data_path=updated_input_data.input_data_path,
|
|
159
|
+
input_data_index=index,
|
|
160
|
+
)
|
|
161
|
+
except Exception:
|
|
162
|
+
logger.warning(f"{index + 1}件目 :: input_data_id='{updated_input_data.input_data_id}'の入力データを更新するのに失敗しました。", exc_info=True)
|
|
163
|
+
return UpdateResult.FAILED
|
|
164
|
+
|
|
165
|
+
def update_input_data_list_in_parallel(
|
|
166
|
+
self,
|
|
167
|
+
project_id: str,
|
|
168
|
+
updated_input_data_list: list[UpdatedInputData],
|
|
169
|
+
parallelism: int,
|
|
170
|
+
) -> None:
|
|
171
|
+
"""複数の入力データを並列的に更新します。"""
|
|
172
|
+
|
|
173
|
+
logger.info(f"{len(updated_input_data_list)} 件の入力データを更新します。{parallelism}個のプロセスを使用して並列実行します。")
|
|
174
|
+
|
|
175
|
+
partial_func = partial(self._update_input_data_wrapper, project_id=project_id)
|
|
176
|
+
with multiprocessing.Pool(parallelism) as pool:
|
|
177
|
+
result_list = pool.map(partial_func, enumerate(updated_input_data_list))
|
|
178
|
+
success_count = len([e for e in result_list if e == UpdateResult.SUCCESS])
|
|
179
|
+
skipped_count = len([e for e in result_list if e == UpdateResult.SKIPPED])
|
|
180
|
+
failed_count = len([e for e in result_list if e == UpdateResult.FAILED])
|
|
181
|
+
|
|
182
|
+
logger.info(f"{success_count} / {len(updated_input_data_list)} 件の入力データを更新しました。(成功: {success_count}件, スキップ: {skipped_count}件, 失敗: {failed_count}件)")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def create_updated_input_data_list_from_dict(input_data_dict_list: list[dict[str, str]]) -> list[UpdatedInputData]:
|
|
186
|
+
return [UpdatedInputData.from_dict(e) for e in input_data_dict_list]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def create_updated_input_data_list_from_csv(csv_file: Path) -> list[UpdatedInputData]:
|
|
190
|
+
"""入力データの情報が記載されているCSVを読み込み、UpdatedInputDataのlistを返します。
|
|
191
|
+
CSVには以下の列が存在します。
|
|
192
|
+
* input_data_id (必須)
|
|
193
|
+
* input_data_name (任意)
|
|
194
|
+
* input_data_path (任意)
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
csv_file (Path): CSVファイルのパス
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
更新対象の入力データのlist
|
|
201
|
+
"""
|
|
202
|
+
df_input_data = pandas.read_csv(
|
|
203
|
+
csv_file,
|
|
204
|
+
# 文字列として読み込むようにする
|
|
205
|
+
dtype={"input_data_id": "string", "input_data_name": "string", "input_data_path": "string"},
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
input_data_dict_list = df_input_data.to_dict("records")
|
|
209
|
+
return [UpdatedInputData.from_dict(e) for e in input_data_dict_list]
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
CLI_COMMON_MESSAGE = "annofabcli input_data update: error:"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class UpdateInputData(CommandLine):
|
|
216
|
+
@staticmethod
|
|
217
|
+
def validate(args: argparse.Namespace) -> bool:
|
|
218
|
+
if args.parallelism is not None and not args.yes:
|
|
219
|
+
print( # noqa: T201
|
|
220
|
+
f"{CLI_COMMON_MESSAGE} argument --parallelism: '--parallelism'を指定するときは、必ず ``--yes`` を指定してください。",
|
|
221
|
+
file=sys.stderr,
|
|
222
|
+
)
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
return True
|
|
226
|
+
|
|
227
|
+
def main(self) -> None:
|
|
228
|
+
args = self.args
|
|
229
|
+
if not self.validate(args):
|
|
230
|
+
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
231
|
+
|
|
232
|
+
main_obj = UpdateInputDataMain(self.service, all_yes=self.all_yes)
|
|
233
|
+
|
|
234
|
+
if args.csv is not None:
|
|
235
|
+
updated_input_data_list = create_updated_input_data_list_from_csv(args.csv)
|
|
236
|
+
|
|
237
|
+
elif args.json is not None:
|
|
238
|
+
input_data_dict_list = get_json_from_args(args.json)
|
|
239
|
+
if not isinstance(input_data_dict_list, list):
|
|
240
|
+
print(f"{CLI_COMMON_MESSAGE} JSON形式が不正です。オブジェクトの配列を指定してください。", file=sys.stderr) # noqa: T201
|
|
241
|
+
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
242
|
+
updated_input_data_list = create_updated_input_data_list_from_dict(input_data_dict_list)
|
|
243
|
+
else:
|
|
244
|
+
raise RuntimeError("argparse により相互排他が保証されているため、ここには到達しません")
|
|
245
|
+
|
|
246
|
+
project_id: str = args.project_id
|
|
247
|
+
if args.parallelism is not None:
|
|
248
|
+
main_obj.update_input_data_list_in_parallel(project_id, updated_input_data_list=updated_input_data_list, parallelism=args.parallelism)
|
|
249
|
+
else:
|
|
250
|
+
main_obj.update_input_data_list_sequentially(project_id, updated_input_data_list=updated_input_data_list)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def main(args: argparse.Namespace) -> None:
|
|
254
|
+
service = build_annofabapi_resource_and_login(args)
|
|
255
|
+
facade = AnnofabApiFacade(service)
|
|
256
|
+
UpdateInputData(service, facade, args).main()
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
260
|
+
argument_parser = ArgumentParser(parser)
|
|
261
|
+
argument_parser.add_project_id()
|
|
262
|
+
|
|
263
|
+
file_group = parser.add_mutually_exclusive_group(required=True)
|
|
264
|
+
file_group.add_argument(
|
|
265
|
+
"--csv",
|
|
266
|
+
type=Path,
|
|
267
|
+
help=(
|
|
268
|
+
"更新対象の入力データと更新後の値が記載されたCSVファイルのパスを指定します。\n"
|
|
269
|
+
"CSVのフォーマットは以下の通りです。"
|
|
270
|
+
"\n"
|
|
271
|
+
" * ヘッダ行あり, カンマ区切り\n"
|
|
272
|
+
" * input_data_id (required)\n"
|
|
273
|
+
" * input_data_name (optional)\n"
|
|
274
|
+
" * input_data_path (optional)\n"
|
|
275
|
+
"更新しないプロパティは、セルの値を空欄にしてください。\n"
|
|
276
|
+
),
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
JSON_SAMPLE = '[{"input_data_id":"id1","input_data_name":"new_name1"},{"input_data_id":"id2","input_data_path":"new_path2"}]' # noqa: N806
|
|
280
|
+
file_group.add_argument(
|
|
281
|
+
"--json",
|
|
282
|
+
type=str,
|
|
283
|
+
help=(
|
|
284
|
+
"更新対象の入力データと更新後の値をJSON形式で指定します。\n"
|
|
285
|
+
"JSONの各キーは ``--csv`` に渡すCSVの各列に対応しています。\n"
|
|
286
|
+
"``file://`` を先頭に付けるとjsonファイルを指定できます。\n"
|
|
287
|
+
f"(ex) ``{JSON_SAMPLE}`` \n"
|
|
288
|
+
"更新しないプロパティは、キーを記載しないか値をnullにしてください。\n"
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
parser.add_argument(
|
|
293
|
+
"--parallelism",
|
|
294
|
+
type=int,
|
|
295
|
+
choices=PARALLELISM_CHOICES,
|
|
296
|
+
help="使用するプロセス数(並列度)。指定しない場合は、逐次的に処理します。指定する場合は ``--yes`` も一緒に指定する必要があります。",
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
parser.set_defaults(subcommand_func=main)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
|
|
303
|
+
subcommand_name = "update"
|
|
304
|
+
subcommand_help = "入力データの名前または入力データのパスを更新します。"
|
|
305
|
+
epilog = "オーナロールを持つユーザで実行してください。"
|
|
306
|
+
parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, epilog=epilog)
|
|
307
|
+
parse_args(parser)
|
|
308
|
+
return parser
|
|
@@ -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,9 +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
|
|
11
|
+
import annofabcli.project.update_configuration
|
|
12
|
+
import annofabcli.project.update_project
|
|
10
13
|
from annofabcli.common.cli import add_parser as common_add_parser
|
|
11
14
|
|
|
12
15
|
|
|
@@ -17,9 +20,12 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
17
20
|
annofabcli.project.change_organization_of_project.add_parser(subparsers)
|
|
18
21
|
annofabcli.project.change_project_status.add_parser(subparsers)
|
|
19
22
|
annofabcli.project.copy_project.add_parser(subparsers)
|
|
23
|
+
annofabcli.project.create_project.add_parser(subparsers)
|
|
20
24
|
annofabcli.project.diff_projects.add_parser(subparsers)
|
|
21
25
|
annofabcli.project.list_project.add_parser(subparsers)
|
|
22
26
|
annofabcli.project.put_project.add_parser(subparsers)
|
|
27
|
+
annofabcli.project.update_configuration.add_parser(subparsers)
|
|
28
|
+
annofabcli.project.update_project.add_parser(subparsers)
|
|
23
29
|
|
|
24
30
|
|
|
25
31
|
def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
|