annofabcli 1.109.0__py3-none-any.whl → 1.111.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/list_single_point_annotation.py +317 -0
- annofabcli/annotation_zip/subcommand_annotation_zip.py +6 -0
- annofabcli/annotation_zip/validate_annotation.py +393 -0
- annofabcli/common/download.py +97 -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/task/put_tasks.py +1 -1
- {annofabcli-1.109.0.dist-info → annofabcli-1.111.0.dist-info}/METADATA +1 -1
- {annofabcli-1.109.0.dist-info → annofabcli-1.111.0.dist-info}/RECORD +23 -19
- {annofabcli-1.109.0.dist-info → annofabcli-1.111.0.dist-info}/WHEEL +0 -0
- {annofabcli-1.109.0.dist-info → annofabcli-1.111.0.dist-info}/entry_points.txt +0 -0
- {annofabcli-1.109.0.dist-info → annofabcli-1.111.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
|
|
@@ -230,10 +230,9 @@ class ListAnnotationArea(CommandLine):
|
|
|
230
230
|
|
|
231
231
|
def download_and_print_annotation_area(project_id: str, temp_dir: Path, *, is_latest: bool, annotation_path: Optional[Path]) -> None:
|
|
232
232
|
if annotation_path is None:
|
|
233
|
-
annotation_path =
|
|
234
|
-
downloading_obj.download_annotation_zip(
|
|
233
|
+
annotation_path = downloading_obj.download_annotation_zip_to_dir(
|
|
235
234
|
project_id,
|
|
236
|
-
|
|
235
|
+
temp_dir,
|
|
237
236
|
is_latest=is_latest,
|
|
238
237
|
)
|
|
239
238
|
print_annotation_area(
|
|
@@ -571,17 +571,15 @@ class ListAnnotationAttributeFilledCount(CommandLine):
|
|
|
571
571
|
|
|
572
572
|
downloading_obj = DownloadingFile(self.service)
|
|
573
573
|
|
|
574
|
-
|
|
575
|
-
# https://qiita.com/yuji38kwmt/items/c6f50e1fc03dafdcdda0 参考
|
|
576
|
-
with tempfile.TemporaryDirectory() as str_temp_dir:
|
|
574
|
+
def download_and_process_annotation(temp_dir: Path, *, is_latest: bool, annotation_path: Optional[Path]) -> None:
|
|
577
575
|
# タスク全件ファイルは、フレーム番号を参照するのに利用する
|
|
578
576
|
if project_id is not None and group_by == GroupBy.INPUT_DATA_ID:
|
|
579
577
|
# group_byで条件を絞り込んでいる理由:
|
|
580
578
|
# タスクIDで集計する際は、フレーム番号は出力しないので、タスク全件ファイルをダウンロードする必要はないため
|
|
581
|
-
task_json_path =
|
|
582
|
-
downloading_obj.download_task_json(
|
|
579
|
+
task_json_path = downloading_obj.download_task_json_to_dir(
|
|
583
580
|
project_id,
|
|
584
|
-
|
|
581
|
+
temp_dir,
|
|
582
|
+
is_latest=is_latest,
|
|
585
583
|
)
|
|
586
584
|
else:
|
|
587
585
|
task_json_path = None
|
|
@@ -600,16 +598,37 @@ class ListAnnotationAttributeFilledCount(CommandLine):
|
|
|
600
598
|
|
|
601
599
|
if annotation_path is None:
|
|
602
600
|
assert project_id is not None
|
|
603
|
-
annotation_path =
|
|
604
|
-
downloading_obj.download_annotation_zip(
|
|
601
|
+
annotation_path = downloading_obj.download_annotation_zip_to_dir(
|
|
605
602
|
project_id,
|
|
606
|
-
|
|
607
|
-
is_latest=
|
|
603
|
+
temp_dir,
|
|
604
|
+
is_latest=is_latest,
|
|
608
605
|
)
|
|
609
606
|
func(annotation_path=annotation_path)
|
|
610
607
|
else:
|
|
611
608
|
func(annotation_path=annotation_path)
|
|
612
609
|
|
|
610
|
+
if project_id is not None:
|
|
611
|
+
if args.temp_dir is not None:
|
|
612
|
+
download_and_process_annotation(temp_dir=args.temp_dir, is_latest=args.latest, annotation_path=annotation_path)
|
|
613
|
+
else:
|
|
614
|
+
with tempfile.TemporaryDirectory() as str_temp_dir:
|
|
615
|
+
download_and_process_annotation(temp_dir=Path(str_temp_dir), is_latest=args.latest, annotation_path=annotation_path)
|
|
616
|
+
else:
|
|
617
|
+
# プロジェクトIDが指定されていない場合は、アノテーションパスが必須なので、一時ディレクトリは不要
|
|
618
|
+
assert annotation_path is not None
|
|
619
|
+
func = partial(
|
|
620
|
+
main_obj.print_annotation_count,
|
|
621
|
+
project_id=project_id,
|
|
622
|
+
task_json_path=None,
|
|
623
|
+
group_by=group_by,
|
|
624
|
+
output_format=output_format,
|
|
625
|
+
output_file=output_file,
|
|
626
|
+
target_task_ids=task_id_list,
|
|
627
|
+
task_query=task_query,
|
|
628
|
+
include_flag_attribute=args.include_flag_attribute,
|
|
629
|
+
)
|
|
630
|
+
func(annotation_path=annotation_path)
|
|
631
|
+
|
|
613
632
|
|
|
614
633
|
def main(args: argparse.Namespace) -> None:
|
|
615
634
|
service = build_annofabapi_resource_and_login(args)
|
|
@@ -671,6 +690,12 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
671
690
|
help="``--annotation`` を指定しないとき、最新のアノテーションzipを参照します。このオプションを指定すると、アノテーションzipを更新するのに数分待ちます。",
|
|
672
691
|
)
|
|
673
692
|
|
|
693
|
+
parser.add_argument(
|
|
694
|
+
"--temp_dir",
|
|
695
|
+
type=Path,
|
|
696
|
+
help="指定したディレクトリに、アノテーションZIPなどの一時ファイルをダウンロードします。",
|
|
697
|
+
)
|
|
698
|
+
|
|
674
699
|
parser.set_defaults(subcommand_func=main)
|
|
675
700
|
|
|
676
701
|
|