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