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.
- annofabcli/annotation_zip/list_annotation_bounding_box_2d.py +49 -37
- annofabcli/annotation_zip/list_range_annotation.py +308 -0
- annofabcli/annotation_zip/subcommand_annotation_zip.py +4 -0
- annofabcli/annotation_zip/validate_annotation.py +393 -0
- annofabcli/common/download.py +97 -0
- 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/statistics/list_annotation_area.py +2 -3
- annofabcli/statistics/list_annotation_attribute_filled_count.py +35 -10
- annofabcli/statistics/list_annotation_count.py +39 -14
- annofabcli/statistics/list_annotation_duration.py +4 -6
- annofabcli/statistics/summarize_task_count.py +53 -33
- annofabcli/statistics/summarize_task_count_by_task_id_group.py +30 -13
- annofabcli/statistics/summarize_task_count_by_user.py +32 -15
- annofabcli/statistics/visualization/dataframe/annotation_count.py +31 -3
- annofabcli/statistics/visualization/dataframe/annotation_duration.py +121 -0
- annofabcli/statistics/visualize_statistics.py +83 -5
- annofabcli/task/complete_tasks.py +2 -2
- {annofabcli-1.108.0.dist-info → annofabcli-1.110.0.dist-info}/METADATA +1 -1
- {annofabcli-1.108.0.dist-info → annofabcli-1.110.0.dist-info}/RECORD +25 -20
- {annofabcli-1.108.0.dist-info → annofabcli-1.110.0.dist-info}/WHEEL +0 -0
- {annofabcli-1.108.0.dist-info → annofabcli-1.110.0.dist-info}/entry_points.txt +0 -0
- {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
|
annofabcli/common/download.py
CHANGED
|
@@ -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
|