annofabcli 1.108.0__py3-none-any.whl → 1.110.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.
Files changed (25) hide show
  1. annofabcli/annotation_zip/list_annotation_bounding_box_2d.py +49 -37
  2. annofabcli/annotation_zip/list_range_annotation.py +308 -0
  3. annofabcli/annotation_zip/subcommand_annotation_zip.py +4 -0
  4. annofabcli/annotation_zip/validate_annotation.py +393 -0
  5. annofabcli/common/download.py +97 -0
  6. annofabcli/project/create_project.py +151 -0
  7. annofabcli/project/put_project.py +14 -129
  8. annofabcli/project/subcommand_project.py +4 -0
  9. annofabcli/project/update_project.py +298 -0
  10. annofabcli/statistics/list_annotation_area.py +2 -3
  11. annofabcli/statistics/list_annotation_attribute_filled_count.py +35 -10
  12. annofabcli/statistics/list_annotation_count.py +39 -14
  13. annofabcli/statistics/list_annotation_duration.py +4 -6
  14. annofabcli/statistics/summarize_task_count.py +53 -33
  15. annofabcli/statistics/summarize_task_count_by_task_id_group.py +30 -13
  16. annofabcli/statistics/summarize_task_count_by_user.py +32 -15
  17. annofabcli/statistics/visualization/dataframe/annotation_count.py +31 -3
  18. annofabcli/statistics/visualization/dataframe/annotation_duration.py +121 -0
  19. annofabcli/statistics/visualize_statistics.py +83 -5
  20. annofabcli/task/complete_tasks.py +2 -2
  21. {annofabcli-1.108.0.dist-info → annofabcli-1.110.0.dist-info}/METADATA +1 -1
  22. {annofabcli-1.108.0.dist-info → annofabcli-1.110.0.dist-info}/RECORD +25 -20
  23. {annofabcli-1.108.0.dist-info → annofabcli-1.110.0.dist-info}/WHEEL +0 -0
  24. {annofabcli-1.108.0.dist-info → annofabcli-1.110.0.dist-info}/entry_points.txt +0 -0
  25. {annofabcli-1.108.0.dist-info → annofabcli-1.110.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,393 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import importlib.util
5
+ import json
6
+ import logging
7
+ import sys
8
+ import tempfile
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Any, Callable, Optional
12
+
13
+ import annofabapi
14
+ import pandas
15
+ from annofabapi.models import ProjectMemberRole
16
+
17
+ import annofabcli.common.cli
18
+ from annofabcli.common.annofab.annotation_zip import lazy_parse_simple_annotation_by_input_data
19
+ from annofabcli.common.cli import COMMAND_LINE_ERROR_STATUS_CODE, ArgumentParser, CommandLine, build_annofabapi_resource_and_login
20
+ from annofabcli.common.download import DownloadingFile
21
+ from annofabcli.common.enums import FormatArgument
22
+ from annofabcli.common.facade import AnnofabApiFacade, TaskQuery, match_annotation_with_task_query
23
+ from annofabcli.common.utils import print_csv, print_json
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ @dataclass
29
+ class ValidationResult:
30
+ """バリデーション結果を格納するクラス"""
31
+
32
+ project_id: str
33
+ task_id: str
34
+ input_data_id: str
35
+ input_data_name: str
36
+ annotation_id: str
37
+ label_name: str
38
+ function_name: str
39
+ is_valid: bool
40
+ error_message: Optional[str] = None
41
+ detail_data: Optional[dict[str, Any]] = None
42
+
43
+
44
+ class ValidationFunctionLoader:
45
+ """ユーザー定義のバリデーション関数をロードするクラス"""
46
+
47
+ def __init__(self, validation_file_path: Path) -> None:
48
+ """
49
+ Args:
50
+ validation_file_path: バリデーション関数が定義されたPythonファイルのパス
51
+ """
52
+ self.validation_file_path = validation_file_path
53
+
54
+ def load_validation_functions(self) -> dict[str, Callable[[dict[str, Any]], bool]]:
55
+ """
56
+ バリデーション関数をロードして辞書で返す。
57
+
58
+ バリデーション関数は以下のシグネチャを持つ必要がある:
59
+ ```python
60
+ def validate_detail(detail: dict[str, Any]) -> bool:
61
+ # バリデーションロジック
62
+ return True # 正常な場合
63
+ ```
64
+
65
+ Returns:
66
+ 関数名をキーとするバリデーション関数の辞書
67
+
68
+ Raises:
69
+ ImportError: バリデーションファイルの読み込みに失敗した場合
70
+ """
71
+ if not self.validation_file_path.exists():
72
+ msg = f"バリデーションファイルが存在しません: {self.validation_file_path}"
73
+ raise FileNotFoundError(msg)
74
+
75
+ spec = importlib.util.spec_from_file_location("validation_module", self.validation_file_path)
76
+ if spec is None or spec.loader is None:
77
+ msg = f"バリデーションファイルの読み込みに失敗しました: {self.validation_file_path}"
78
+ raise ImportError(msg)
79
+
80
+ module = importlib.util.module_from_spec(spec)
81
+ spec.loader.exec_module(module)
82
+
83
+ validation_functions = {}
84
+ for name in dir(module):
85
+ obj = getattr(module, name)
86
+ if callable(obj) and name.startswith("validate_") and not name.startswith("__"):
87
+ validation_functions[name] = obj
88
+ logger.debug(f"バリデーション関数を読み込みました: {name}")
89
+
90
+ if not validation_functions:
91
+ logger.warning(f"バリデーション関数が見つかりません: {self.validation_file_path}")
92
+
93
+ return validation_functions
94
+
95
+
96
+ class AnnotationValidator:
97
+ """アノテーションのバリデーションを実行するクラス"""
98
+
99
+ def __init__(
100
+ self,
101
+ service: annofabapi.Resource,
102
+ project_id: str,
103
+ validation_functions: dict[str, Callable[[dict[str, Any]], bool]],
104
+ task_query: Optional[TaskQuery] = None,
105
+ ) -> None:
106
+ self.service = service
107
+ self.project_id = project_id
108
+ self.validation_functions = validation_functions
109
+ self.task_query = task_query
110
+ self.facade = AnnofabApiFacade(service)
111
+
112
+ def validate_annotations(self, annotation_zip_path: Path) -> list[ValidationResult]:
113
+ """
114
+ アノテーションZIPファイルを検証する。
115
+
116
+ Args:
117
+ annotation_zip_path: アノテーションZIPファイルのパス
118
+
119
+ Returns:
120
+ バリデーション結果のリスト
121
+ """
122
+ logger.info("アノテーションの検証を開始します。")
123
+ results: list[ValidationResult] = []
124
+
125
+ count_validated_annotations = 0
126
+
127
+ # アノテーションZIPからアノテーション詳細情報を取得
128
+ for parser in lazy_parse_simple_annotation_by_input_data(annotation_zip_path):
129
+ simple_annotation_detail = parser.load_json()
130
+
131
+ # タスク検索条件でフィルタリング
132
+ if self.task_query is not None and not match_annotation_with_task_query(simple_annotation_detail, self.task_query):
133
+ continue
134
+
135
+ task_id = simple_annotation_detail["task_id"]
136
+ input_data_id = simple_annotation_detail["input_data_id"]
137
+ input_data_name = simple_annotation_detail["input_data_name"]
138
+
139
+ details = simple_annotation_detail["details"]
140
+ for detail in details:
141
+ annotation_id = detail["annotation_id"]
142
+ label_name = detail["label"]
143
+
144
+ count_validated_annotations += 1
145
+ if count_validated_annotations % 1000 == 0:
146
+ logger.info(f"{count_validated_annotations} 件のアノテーションを検証しました。")
147
+
148
+ # 各バリデーション関数を実行
149
+ for function_name, validation_function in self.validation_functions.items():
150
+ try:
151
+ is_valid = validation_function(detail)
152
+ result = ValidationResult(
153
+ project_id=self.project_id,
154
+ task_id=task_id,
155
+ input_data_id=input_data_id,
156
+ input_data_name=input_data_name,
157
+ annotation_id=annotation_id,
158
+ label_name=label_name,
159
+ function_name=function_name,
160
+ is_valid=is_valid,
161
+ detail_data=detail if not is_valid else None,
162
+ )
163
+ results.append(result)
164
+
165
+ if not is_valid:
166
+ logger.debug(f"バリデーション失敗: task_id={task_id}, input_data_id={input_data_id}, annotation_id={annotation_id}, function={function_name}")
167
+
168
+ except Exception as e:
169
+ error_message = f"バリデーション関数でエラーが発生しました: {e!s}"
170
+ logger.warning(error_message, exc_info=True)
171
+ result = ValidationResult(
172
+ project_id=self.project_id,
173
+ task_id=task_id,
174
+ input_data_id=input_data_id,
175
+ input_data_name=input_data_name,
176
+ annotation_id=annotation_id,
177
+ label_name=label_name,
178
+ function_name=function_name,
179
+ is_valid=False,
180
+ error_message=error_message,
181
+ detail_data=detail,
182
+ )
183
+ results.append(result)
184
+
185
+ logger.info(f"検証完了: {count_validated_annotations} 件のアノテーションを処理しました。")
186
+ return results
187
+
188
+
189
+ class ValidateAnnotation(CommandLine):
190
+ """
191
+ アノテーションZIPをユーザー定義のバリデーション関数で検証する。
192
+ """
193
+
194
+ def __init__(self, service, facade, args) -> None: # noqa: ANN001
195
+ super().__init__(service, facade, args)
196
+
197
+ @staticmethod
198
+ def validate(args: argparse.Namespace) -> bool:
199
+ COMMON_MESSAGE = "annofabcli annotation_zip validate: error:" # noqa: N806
200
+
201
+ validation_file_path = Path(args.validation_file)
202
+ if not validation_file_path.exists():
203
+ print( # noqa: T201
204
+ f"{COMMON_MESSAGE} バリデーションファイルが存在しません: {validation_file_path}",
205
+ file=sys.stderr,
206
+ )
207
+ return False
208
+
209
+ return True
210
+
211
+ def main(self) -> None:
212
+ args = self.args
213
+ if not self.validate(args):
214
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
215
+
216
+ project_id = args.project_id
217
+
218
+ # プロジェクトメンバーかどうかを確認
219
+ self.facade.validate_project(project_id, project_member_roles=[ProjectMemberRole.OWNER, ProjectMemberRole.TRAINING_DATA_USER])
220
+
221
+ # バリデーション関数をロード
222
+ try:
223
+ function_loader = ValidationFunctionLoader(Path(args.validation_file))
224
+ validation_functions = function_loader.load_validation_functions()
225
+ except (ImportError, FileNotFoundError):
226
+ logger.exception("バリデーション関数の読み込みに失敗しました")
227
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
228
+
229
+ if not validation_functions:
230
+ logger.error("有効なバリデーション関数が見つかりません。")
231
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
232
+
233
+ logger.info(f"読み込んだバリデーション関数: {list(validation_functions.keys())}")
234
+
235
+ # タスク検索クエリ
236
+ task_query: Optional[TaskQuery] = None
237
+ if args.task_query is not None:
238
+ dict_task_query = annofabcli.common.cli.get_json_from_args(args.task_query)
239
+ task_query = TaskQuery.from_dict(dict_task_query)
240
+
241
+ # アノテーションZIPファイルのダウンロード
242
+ if args.annotation_zip is None:
243
+ # 最新のアノテーションZIPをダウンロード
244
+ with tempfile.TemporaryDirectory() as str_temp_dir:
245
+ temp_dir = Path(str_temp_dir)
246
+ logger.info("最新のアノテーションZIPをダウンロードしています...")
247
+
248
+ downloading_obj = DownloadingFile(self.service)
249
+ annotation_zip_path = downloading_obj.download_annotation_zip_to_dir(project_id, temp_dir)
250
+
251
+ # バリデーション実行
252
+ validator = AnnotationValidator(
253
+ service=self.service,
254
+ project_id=project_id,
255
+ validation_functions=validation_functions,
256
+ task_query=task_query,
257
+ )
258
+ results = validator.validate_annotations(annotation_zip_path)
259
+ else:
260
+ # ローカルのアノテーションZIPファイルを使用
261
+ annotation_zip_path = Path(args.annotation_zip)
262
+ if not annotation_zip_path.exists():
263
+ logger.error(f"アノテーションZIPファイルが存在しません: {annotation_zip_path}")
264
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
265
+
266
+ validator = AnnotationValidator(
267
+ service=self.service,
268
+ project_id=project_id,
269
+ validation_functions=validation_functions,
270
+ task_query=task_query,
271
+ )
272
+ results = validator.validate_annotations(annotation_zip_path)
273
+
274
+ # 結果の出力
275
+ self._output_results(results, args.format, args.output_file)
276
+
277
+ # サマリー表示
278
+ self._print_summary(results)
279
+
280
+ def _output_results(self, results: list[ValidationResult], format_type: str, output_file: Optional[Path]) -> None:
281
+ """バリデーション結果を出力する"""
282
+ if not results:
283
+ logger.info("バリデーション結果はありません。")
284
+ return
285
+
286
+ # DataFrameに変換
287
+ df_results = pandas.DataFrame(
288
+ [
289
+ {
290
+ "project_id": result.project_id,
291
+ "task_id": result.task_id,
292
+ "input_data_id": result.input_data_id,
293
+ "input_data_name": result.input_data_name,
294
+ "annotation_id": result.annotation_id,
295
+ "label_name": result.label_name,
296
+ "function_name": result.function_name,
297
+ "is_valid": result.is_valid,
298
+ "error_message": result.error_message,
299
+ "detail_data": json.dumps(result.detail_data, ensure_ascii=False) if result.detail_data else None,
300
+ }
301
+ for result in results
302
+ ]
303
+ )
304
+
305
+ # フォーマットに応じて出力
306
+ if format_type == "json":
307
+ if output_file is not None:
308
+ print_json(results, output=output_file, is_pretty=True)
309
+ else:
310
+ print_json(results, is_pretty=True)
311
+ elif output_file is not None:
312
+ print_csv(df_results, output=output_file)
313
+ else:
314
+ print_csv(df_results)
315
+
316
+ def _print_summary(self, results: list[ValidationResult]) -> None:
317
+ """バリデーション結果のサマリーを表示する"""
318
+ if not results:
319
+ return
320
+
321
+ df_results = pandas.DataFrame(
322
+ [
323
+ {
324
+ "function_name": result.function_name,
325
+ "is_valid": result.is_valid,
326
+ }
327
+ for result in results
328
+ ]
329
+ )
330
+
331
+ summary = df_results.groupby(["function_name", "is_valid"]).size().reset_index(name="count").pivot_table(index="function_name", columns="is_valid", values="count", fill_value=0)
332
+
333
+ logger.info("=== バリデーション結果サマリー ===")
334
+ for function_name in summary.index:
335
+ valid_count = summary.loc[function_name].get(True, 0)
336
+ invalid_count = summary.loc[function_name].get(False, 0)
337
+ total_count = valid_count + invalid_count
338
+ logger.info(f"{function_name}: 正常 {valid_count} / 異常 {invalid_count} / 合計 {total_count}")
339
+
340
+
341
+ def main(args: argparse.Namespace) -> None:
342
+ service = build_annofabapi_resource_and_login(args)
343
+ facade = AnnofabApiFacade(service)
344
+ ValidateAnnotation(service, facade, args).main()
345
+
346
+
347
+ def parse_args(parser: argparse.ArgumentParser) -> None:
348
+ argument_parser = ArgumentParser(parser)
349
+
350
+ argument_parser.add_project_id()
351
+
352
+ parser.add_argument(
353
+ "--validation_file",
354
+ type=Path,
355
+ required=True,
356
+ help=(
357
+ "バリデーション関数が定義されたPythonファイルのパスを指定してください。\n"
358
+ "ファイル内で 'validate_' から始まる関数が自動的に検出され、バリデーション関数として使用されます。\n"
359
+ "バリデーション関数は 'def validate_xxx(detail: dict[str, Any]) -> bool:' のシグネチャを持つ必要があります。"
360
+ ),
361
+ )
362
+
363
+ parser.add_argument(
364
+ "--annotation_zip",
365
+ type=Path,
366
+ help="検証対象のアノテーションZIPファイルのパスを指定してください。指定しない場合は、最新のアノテーションZIPをダウンロードして使用します。",
367
+ )
368
+
369
+ parser.add_argument(
370
+ "--task_query",
371
+ type=str,
372
+ help=(
373
+ "タスクの検索クエリをJSON形式で指定します。\n"
374
+ "``file://`` を先頭に付けると、JSON形式のファイルを指定できます。\n"
375
+ "クエリのキーは、``task_id`` , ``phase`` , ``phase_stage`` , ``status`` のみです。"
376
+ ),
377
+ )
378
+
379
+ argument_parser.add_format(choices=[FormatArgument.CSV, FormatArgument.JSON], default=FormatArgument.CSV)
380
+ argument_parser.add_output()
381
+
382
+ parser.set_defaults(subcommand_func=main)
383
+
384
+
385
+ def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
386
+ subcommand_name = "validate"
387
+ subcommand_help = "ユーザー定義のバリデーション関数でアノテーションZIPを検証します。"
388
+ description = "ユーザー定義のバリデーション関数でアノテーションZIPを検証します。\nバリデーション関数は 'validate_' から始まる関数名で定義してください。"
389
+ epilog = "アノテーションユーザまたはオーナロールを持つユーザで実行してください。"
390
+
391
+ parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog)
392
+ parse_args(parser)
393
+ return parser
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import datetime
2
3
  import logging.config
3
4
  from functools import partial
4
5
  from pathlib import Path
@@ -30,6 +31,11 @@ def _get_annofab_error_message(http_error: requests.HTTPError) -> Optional[str]:
30
31
  return errors[0].get("message")
31
32
 
32
33
 
34
+ def get_filename(project_id: str, file_type: str, extension: str) -> str:
35
+ timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S") # noqa: DTZ005
36
+ return f"{timestamp}--{project_id}--{file_type}.{extension}"
37
+
38
+
33
39
  class DownloadingFile:
34
40
  def __init__(self, service: annofabapi.Resource) -> None:
35
41
  self.service = service
@@ -308,3 +314,94 @@ class DownloadingFile:
308
314
  if e.response.status_code == requests.codes.not_found:
309
315
  raise DownloadingFileNotFoundError(f"project_id='{project_id}'のプロジェクトに、コメント全件ファイルが存在しないため、ダウンロードできませんでした。") from e
310
316
  raise e # noqa: TRY201
317
+
318
+ # 統一された命名規則でファイルをダウンロードする関数群
319
+ def download_annotation_zip_to_dir(
320
+ self,
321
+ project_id: str,
322
+ output_dir: Path,
323
+ *,
324
+ is_latest: bool = False,
325
+ wait_options: Optional[WaitOptions] = None,
326
+ ) -> Path:
327
+ """
328
+ アノテーションZIPをoutput_dirに統一された命名規則でダウンロードする。
329
+
330
+ Args:
331
+ project_id: プロジェクトID
332
+ output_dir: 出力ディレクトリ
333
+ is_latest: 最新版をダウンロードするかどうか
334
+ wait_options: 待機オプション
335
+
336
+ Returns:
337
+ ダウンロードされたファイルのパス
338
+ """
339
+ dest_path = output_dir / get_filename(project_id, "annotation", "zip")
340
+
341
+ self.download_annotation_zip(
342
+ project_id,
343
+ dest_path=str(dest_path),
344
+ is_latest=is_latest,
345
+ wait_options=wait_options,
346
+ )
347
+ return dest_path
348
+
349
+ def download_task_json_to_dir(
350
+ self,
351
+ project_id: str,
352
+ output_dir: Path,
353
+ *,
354
+ is_latest: bool = False,
355
+ wait_options: Optional[WaitOptions] = None,
356
+ ) -> Path:
357
+ """
358
+ タスクJSONをoutput_dirに統一された命名規則でダウンロードする。
359
+
360
+ Args:
361
+ project_id: プロジェクトID
362
+ output_dir: 出力ディレクトリ
363
+ is_latest: 最新版をダウンロードするかどうか
364
+ wait_options: 待機オプション
365
+
366
+ Returns:
367
+ ダウンロードされたファイルのパス
368
+ """
369
+ dest_path = output_dir / get_filename(project_id, "task", "json")
370
+
371
+ self.download_task_json(
372
+ project_id,
373
+ dest_path=str(dest_path),
374
+ is_latest=is_latest,
375
+ wait_options=wait_options,
376
+ )
377
+ return dest_path
378
+
379
+ def download_input_data_json_to_dir(
380
+ self,
381
+ project_id: str,
382
+ output_dir: Path,
383
+ *,
384
+ is_latest: bool = False,
385
+ wait_options: Optional[WaitOptions] = None,
386
+ ) -> Path:
387
+ """
388
+ 入力データJSONをoutput_dirに統一された命名規則でダウンロードする。
389
+
390
+ Args:
391
+ project_id: プロジェクトID
392
+ output_dir: 出力ディレクトリ
393
+ is_latest: 最新版をダウンロードするかどうか
394
+ wait_options: 待機オプション
395
+
396
+ Returns:
397
+ ダウンロードされたファイルのパス
398
+ """
399
+ dest_path = output_dir / get_filename(project_id, "input_data", "json")
400
+
401
+ self.download_input_data_json(
402
+ project_id,
403
+ dest_path=str(dest_path),
404
+ is_latest=is_latest,
405
+ wait_options=wait_options,
406
+ )
407
+ return dest_path
@@ -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