annofabcli 1.107.1__py3-none-any.whl → 1.109.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- annofabcli/annotation/change_annotation_attributes.py +1 -1
- annofabcli/annotation/create_classification_annotation.py +408 -0
- annofabcli/annotation/delete_annotation.py +50 -14
- annofabcli/annotation/import_annotation.py +7 -8
- annofabcli/annotation/subcommand_annotation.py +2 -0
- annofabcli/input_data/subcommand_input_data.py +2 -2
- annofabcli/input_data/update_input_data.py +308 -0
- annofabcli/project/create_project.py +151 -0
- annofabcli/project/put_project.py +14 -129
- annofabcli/project/subcommand_project.py +6 -0
- annofabcli/project/update_configuration.py +151 -0
- annofabcli/project/update_project.py +298 -0
- annofabcli/statistics/list_video_duration.py +2 -2
- annofabcli/task/complete_tasks.py +20 -20
- {annofabcli-1.107.1.dist-info → annofabcli-1.109.0.dist-info}/METADATA +2 -2
- {annofabcli-1.107.1.dist-info → annofabcli-1.109.0.dist-info}/RECORD +19 -15
- annofabcli/input_data/change_input_data_name.py +0 -245
- {annofabcli-1.107.1.dist-info → annofabcli-1.109.0.dist-info}/WHEEL +0 -0
- {annofabcli-1.107.1.dist-info → annofabcli-1.109.0.dist-info}/entry_points.txt +0 -0
- {annofabcli-1.107.1.dist-info → annofabcli-1.109.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import copy
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
import annofabapi
|
|
7
|
+
|
|
8
|
+
import annofabcli
|
|
9
|
+
from annofabcli.common.cli import CommandLine, CommandLineWithConfirm, build_annofabapi_resource_and_login, get_json_from_args, get_list_from_args
|
|
10
|
+
from annofabcli.common.facade import AnnofabApiFacade
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UpdateProjectConfigurationMain(CommandLineWithConfirm):
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
service: annofabapi.Resource,
|
|
19
|
+
*,
|
|
20
|
+
all_yes: bool = False,
|
|
21
|
+
) -> None:
|
|
22
|
+
self.service = service
|
|
23
|
+
self.facade = AnnofabApiFacade(service)
|
|
24
|
+
super().__init__(all_yes)
|
|
25
|
+
|
|
26
|
+
def update_configuration_for_project(self, project_id: str, configuration: dict[str, Any], *, project_index: Optional[int] = None) -> bool:
|
|
27
|
+
"""
|
|
28
|
+
指定されたプロジェクトの設定を更新する。
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
project_id: プロジェクトID
|
|
32
|
+
configuration: 更新する設定(既存設定に対する部分的な更新)
|
|
33
|
+
project_index: プロジェクトのインデックス(ログメッセージ用)
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
True: プロジェクトの設定を更新した。
|
|
37
|
+
False: 何らかの理由でプロジェクトの設定を更新していない
|
|
38
|
+
"""
|
|
39
|
+
# ログメッセージの先頭の変数
|
|
40
|
+
log_prefix = f"project_id='{project_id}' :: "
|
|
41
|
+
if project_index is not None:
|
|
42
|
+
log_prefix = f"{project_index + 1}件目 :: {log_prefix}"
|
|
43
|
+
|
|
44
|
+
project = self.service.wrapper.get_project_or_none(project_id)
|
|
45
|
+
if project is None:
|
|
46
|
+
logger.warning(f"{log_prefix}プロジェクトは存在しないので、スキップします。")
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
project_name = project["title"]
|
|
50
|
+
|
|
51
|
+
# 既存の設定を取得し、新しい設定をマージする
|
|
52
|
+
current_configuration = project["configuration"]
|
|
53
|
+
updated_configuration = copy.deepcopy(current_configuration)
|
|
54
|
+
updated_configuration.update(configuration)
|
|
55
|
+
|
|
56
|
+
# 設定に変更がない場合はスキップ
|
|
57
|
+
if current_configuration == updated_configuration:
|
|
58
|
+
logger.debug(f"{log_prefix}プロジェクト設定に変更がないため、スキップします。 :: project_name='{project_name}'")
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
if not self.confirm_processing(f"{log_prefix}プロジェクト設定を更新しますか? :: project_name='{project_name}'"):
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
request_body = copy.deepcopy(project)
|
|
65
|
+
request_body["configuration"] = updated_configuration
|
|
66
|
+
request_body["last_updated_datetime"] = project["updated_datetime"]
|
|
67
|
+
request_body["status"] = project["project_status"]
|
|
68
|
+
|
|
69
|
+
_, _ = self.service.api.put_project(project_id, request_body=request_body, query_params={"v": "2"})
|
|
70
|
+
logger.debug(f"{log_prefix}プロジェクト設定を更新しました。 :: project_name='{project_name}'")
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
def update_configuration_for_project_list(self, project_id_list: list[str], configuration: dict[str, Any]) -> None:
|
|
74
|
+
"""
|
|
75
|
+
複数のプロジェクトの設定を更新する。
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
project_id_list: プロジェクトIDのリスト
|
|
79
|
+
configuration: 更新する設定
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
logger.info(f"{len(project_id_list)} 件のプロジェクトの設定を更新します。")
|
|
83
|
+
|
|
84
|
+
success_count = 0
|
|
85
|
+
skip_count = 0
|
|
86
|
+
failure_count = 0
|
|
87
|
+
|
|
88
|
+
for index, project_id in enumerate(project_id_list):
|
|
89
|
+
try:
|
|
90
|
+
if (index + 1) % 1000 == 0:
|
|
91
|
+
logger.info(f"{index + 1} / {len(project_id_list)} 件目のプロジェクトの設定を更新中...")
|
|
92
|
+
|
|
93
|
+
result = self.update_configuration_for_project(project_id, configuration, project_index=index - 1)
|
|
94
|
+
if result:
|
|
95
|
+
success_count += 1
|
|
96
|
+
else:
|
|
97
|
+
skip_count += 1
|
|
98
|
+
|
|
99
|
+
except Exception:
|
|
100
|
+
failure_count += 1
|
|
101
|
+
logger.warning(f"{index + 1}件目 :: project_id='{project_id}'の設定更新で予期しないエラーが発生しました。", exc_info=True)
|
|
102
|
+
|
|
103
|
+
logger.info(f"{success_count}/{len(project_id_list)}件のプロジェクトの設定の更新が完了しました。 :: スキップ: {skip_count}件, 失敗: {failure_count}件")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class UpdateProjectConfiguration(CommandLine):
|
|
107
|
+
def main(self) -> None:
|
|
108
|
+
args = self.args
|
|
109
|
+
project_id_list = get_list_from_args(args.project_id)
|
|
110
|
+
configuration = get_json_from_args(args.configuration)
|
|
111
|
+
|
|
112
|
+
main_obj = UpdateProjectConfigurationMain(self.service, all_yes=args.yes)
|
|
113
|
+
main_obj.update_configuration_for_project_list(project_id_list=project_id_list, configuration=configuration)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def main(args: argparse.Namespace) -> None:
|
|
117
|
+
service = build_annofabapi_resource_and_login(args)
|
|
118
|
+
facade = AnnofabApiFacade(service)
|
|
119
|
+
UpdateProjectConfiguration(service, facade, args).main()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
123
|
+
parser.add_argument(
|
|
124
|
+
"-p",
|
|
125
|
+
"--project_id",
|
|
126
|
+
type=str,
|
|
127
|
+
required=True,
|
|
128
|
+
nargs="+",
|
|
129
|
+
help="変更対象プロジェクトのproject_idを指定します。 ``file://`` を先頭に付けると、project_idの一覧が記載されたファイルを指定できます。",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
parser.add_argument(
|
|
133
|
+
"--configuration",
|
|
134
|
+
type=str,
|
|
135
|
+
required=True,
|
|
136
|
+
help="更新するプロジェクト設定をJSON形式で指定します。既存の設定に対して部分的な更新を行います。"
|
|
137
|
+
"JSONの構造については https://annofab.com/docs/api/#operation/putProject のリクエストボディ'configuration'を参照してください。\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 = "update_configuration"
|
|
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
|
|
@@ -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
|
|
@@ -85,9 +85,9 @@ class ListVideoDuration(CommandLine):
|
|
|
85
85
|
output_format: FormatArgument,
|
|
86
86
|
output_file: Optional[Path],
|
|
87
87
|
) -> None:
|
|
88
|
-
with task_json.open() as f:
|
|
88
|
+
with task_json.open(encoding="utf-8") as f:
|
|
89
89
|
task_list = json.load(f)
|
|
90
|
-
with input_data_json.open() as f:
|
|
90
|
+
with input_data_json.open(encoding="utf-8") as f:
|
|
91
91
|
input_data_list = json.load(f)
|
|
92
92
|
|
|
93
93
|
video_duration_list = get_video_duration_list(task_list=task_list, input_data_list=input_data_list)
|
|
@@ -217,10 +217,10 @@ class CompleteTasksMain(CommandLineWithConfirm):
|
|
|
217
217
|
|
|
218
218
|
unanswered_comment_count_for_task = sum(len(e) for e in unanswered_comment_list_dict.values())
|
|
219
219
|
|
|
220
|
-
logger.debug(f"{task.task_id}
|
|
220
|
+
logger.debug(f"task_id='{task.task_id}' :: 未回答の検査コメントが {unanswered_comment_count_for_task} 件あります。")
|
|
221
221
|
if unanswered_comment_count_for_task > 0: # noqa: SIM102
|
|
222
222
|
if reply_comment is None:
|
|
223
|
-
logger.warning(f"{task.task_id}
|
|
223
|
+
logger.warning(f"task_id='{task.task_id}' :: 未回答の検査コメントに対する返信コメント('--reply_comment')が指定されていないので、スキップします。")
|
|
224
224
|
return False
|
|
225
225
|
|
|
226
226
|
if not self.confirm_processing(f"タスク'{task.task_id}'の教師付フェーズを次のフェーズに進めますか?"):
|
|
@@ -229,7 +229,7 @@ class CompleteTasksMain(CommandLineWithConfirm):
|
|
|
229
229
|
task = self.change_to_working_status(task)
|
|
230
230
|
if unanswered_comment_count_for_task > 0:
|
|
231
231
|
assert reply_comment is not None
|
|
232
|
-
logger.debug(f"{task.task_id}
|
|
232
|
+
logger.debug(f"task_id='{task.task_id}' :: 未回答の検査コメント {unanswered_comment_count_for_task} 件に対して、返信コメントを付与します。")
|
|
233
233
|
for input_data_id, unanswered_comment_list in unanswered_comment_list_dict.items():
|
|
234
234
|
if len(unanswered_comment_list) == 0:
|
|
235
235
|
continue
|
|
@@ -241,7 +241,7 @@ class CompleteTasksMain(CommandLineWithConfirm):
|
|
|
241
241
|
)
|
|
242
242
|
|
|
243
243
|
self.service.wrapper.complete_task(task.project_id, task.task_id, last_updated_datetime=task.updated_datetime)
|
|
244
|
-
logger.info(f"{task.task_id}
|
|
244
|
+
logger.info(f"task_id='{task.task_id}' :: 教師付フェーズから次のフェーズに進めました。")
|
|
245
245
|
return True
|
|
246
246
|
|
|
247
247
|
def complete_task_for_inspection_acceptance_phase(
|
|
@@ -256,10 +256,10 @@ class CompleteTasksMain(CommandLineWithConfirm):
|
|
|
256
256
|
|
|
257
257
|
unprocessed_inspection_count = sum(len(e) for e in unprocessed_inspection_list_dict.values())
|
|
258
258
|
|
|
259
|
-
logger.debug(f"{task.task_id}
|
|
259
|
+
logger.debug(f"task_id='{task.task_id}' :: 未処置の検査コメントが {unprocessed_inspection_count} 件あります。")
|
|
260
260
|
if unprocessed_inspection_count > 0: # noqa: SIM102
|
|
261
261
|
if inspection_status is None:
|
|
262
|
-
logger.warning(f"{task.task_id}
|
|
262
|
+
logger.warning(f"task_id='{task.task_id}' :: 未処置の検査コメントに対する対応方法('--inspection_status')が指定されていないので、スキップします。")
|
|
263
263
|
return False
|
|
264
264
|
|
|
265
265
|
if not self.confirm_processing(f"タスク'{task.task_id}'の検査/受入フェーズを次のフェーズに進めますか?"):
|
|
@@ -269,7 +269,7 @@ class CompleteTasksMain(CommandLineWithConfirm):
|
|
|
269
269
|
|
|
270
270
|
if unprocessed_inspection_count > 0:
|
|
271
271
|
assert inspection_status is not None
|
|
272
|
-
logger.debug(f"{task.task_id}
|
|
272
|
+
logger.debug(f"task_id='{task.task_id}' :: 未処置の検査コメント {unprocessed_inspection_count} 件を、{inspection_status.value} 状態にします。")
|
|
273
273
|
for input_data_id, unprocessed_inspection_list in unprocessed_inspection_list_dict.items():
|
|
274
274
|
if len(unprocessed_inspection_list) == 0:
|
|
275
275
|
continue
|
|
@@ -282,21 +282,21 @@ class CompleteTasksMain(CommandLineWithConfirm):
|
|
|
282
282
|
)
|
|
283
283
|
|
|
284
284
|
self.service.wrapper.complete_task(task.project_id, task.task_id, last_updated_datetime=task.updated_datetime)
|
|
285
|
-
logger.info(f"{task.task_id}
|
|
285
|
+
logger.info(f"task_id='{task.task_id}' :: 検査/受入フェーズを次のフェーズに進めました。")
|
|
286
286
|
return True
|
|
287
287
|
|
|
288
288
|
@staticmethod
|
|
289
289
|
def _validate_task(task: Task, target_phase: TaskPhase, target_phase_stage: int, task_query: Optional[TaskQuery]) -> bool:
|
|
290
290
|
if not (task.phase == target_phase and task.phase_stage == target_phase_stage):
|
|
291
|
-
logger.warning(f"{task.task_id}
|
|
291
|
+
logger.warning(f"task_id='{task.task_id}'のタスクは操作対象のフェーズ、フェーズステージではないため、スキップします。")
|
|
292
292
|
return False
|
|
293
293
|
|
|
294
294
|
if task.status in {TaskStatus.COMPLETE, TaskStatus.WORKING}:
|
|
295
|
-
logger.warning(f"{task.task_id}
|
|
295
|
+
logger.warning(f"task_id='{task.task_id}'のタスクは作業中または完了状態であるため、スキップします。")
|
|
296
296
|
return False
|
|
297
297
|
|
|
298
298
|
if not match_task_with_query(task, task_query):
|
|
299
|
-
logger.debug(f"{task.task_id} は `--task_query` の条件にマッチしないため、スキップします。task_query={task_query}")
|
|
299
|
+
logger.debug(f"task_id='{task.task_id}' は `--task_query` の条件にマッチしないため、スキップします。 :: task_query={task_query}")
|
|
300
300
|
return False
|
|
301
301
|
return True
|
|
302
302
|
|
|
@@ -315,11 +315,11 @@ class CompleteTasksMain(CommandLineWithConfirm):
|
|
|
315
315
|
|
|
316
316
|
dict_task = self.service.wrapper.get_task_or_none(project_id, task_id)
|
|
317
317
|
if dict_task is None:
|
|
318
|
-
logger.warning(f"{logging_prefix}
|
|
318
|
+
logger.warning(f"{logging_prefix} :: task_id='{task_id}'のタスクは存在しないので、スキップします。")
|
|
319
319
|
return False
|
|
320
320
|
|
|
321
321
|
task: Task = Task.from_dict(dict_task)
|
|
322
|
-
logger.info(f"{logging_prefix}
|
|
322
|
+
logger.info(f"{logging_prefix} :: タスク情報 task_id='{task_id}', phase={task.phase.value}, phase_stage={task.phase_stage}, status={task.status.value}")
|
|
323
323
|
if not self._validate_task(task, target_phase=target_phase, target_phase_stage=target_phase_stage, task_query=task_query):
|
|
324
324
|
return False
|
|
325
325
|
|
|
@@ -330,7 +330,7 @@ class CompleteTasksMain(CommandLineWithConfirm):
|
|
|
330
330
|
return self.complete_task_for_inspection_acceptance_phase(task, inspection_status=inspection_status)
|
|
331
331
|
|
|
332
332
|
except Exception: # pylint: disable=broad-except
|
|
333
|
-
logger.warning(f"{task_id}
|
|
333
|
+
logger.warning(f"task_id='{task_id}' :: '{task.phase}'フェーズを次のフェーズへ進めるのに失敗しました。", exc_info=True)
|
|
334
334
|
new_task: Task = Task.from_dict(self.service.wrapper.get_task_or_none(project_id, task_id))
|
|
335
335
|
if new_task.status == TaskStatus.WORKING and new_task.account_id == self.service.api.account_id:
|
|
336
336
|
self.service.wrapper.change_task_status_to_break(project_id, task_id)
|
|
@@ -359,7 +359,7 @@ class CompleteTasksMain(CommandLineWithConfirm):
|
|
|
359
359
|
task_query=task_query,
|
|
360
360
|
)
|
|
361
361
|
except Exception: # pylint: disable=broad-except
|
|
362
|
-
logger.warning(f"
|
|
362
|
+
logger.warning(f"task_id='{task_id}'のタスクのフェーズを完了状態にするのに失敗しました。", exc_info=True)
|
|
363
363
|
return False
|
|
364
364
|
|
|
365
365
|
def complete_task_list( # noqa: ANN201
|
|
@@ -387,7 +387,7 @@ class CompleteTasksMain(CommandLineWithConfirm):
|
|
|
387
387
|
task_query = self.facade.set_account_id_of_task_query(project_id, task_query)
|
|
388
388
|
|
|
389
389
|
project_title = self.facade.get_project_title(project_id)
|
|
390
|
-
logger.info(f"{project_title} のタスク {len(task_id_list)}
|
|
390
|
+
logger.info(f"{project_title} のタスク {len(task_id_list)} 件に対して、'{target_phase.value}'フェーズを次のフェーズに進めます。")
|
|
391
391
|
|
|
392
392
|
success_count = 0
|
|
393
393
|
|
|
@@ -423,10 +423,10 @@ class CompleteTasksMain(CommandLineWithConfirm):
|
|
|
423
423
|
if result:
|
|
424
424
|
success_count += 1
|
|
425
425
|
except Exception: # pylint: disable=broad-except
|
|
426
|
-
logger.warning(f"
|
|
426
|
+
logger.warning(f"task_id='{task_id}'のタスクのフェーズを次のフェーズに進めるのに失敗しました。", exc_info=True)
|
|
427
427
|
continue
|
|
428
428
|
|
|
429
|
-
logger.info(f"{success_count} / {len(task_id_list)}
|
|
429
|
+
logger.info(f"{success_count} / {len(task_id_list)} 件のタスクに対して、'{target_phase.value}'フェーズを次のフェーズに進めました。")
|
|
430
430
|
|
|
431
431
|
|
|
432
432
|
class CompleteTasks(CommandLine):
|
|
@@ -544,9 +544,9 @@ def main(args: argparse.Namespace) -> None:
|
|
|
544
544
|
|
|
545
545
|
def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
|
|
546
546
|
subcommand_name = "complete"
|
|
547
|
-
subcommand_help = "
|
|
547
|
+
subcommand_help = "タスクを次のフェーズに進めます。(教師付の提出、検査/受入の合格)"
|
|
548
548
|
description = (
|
|
549
|
-
"
|
|
549
|
+
"タスクを次のフェーズに進めます。(教師付の提出、検査/受入の合格) "
|
|
550
550
|
"教師付フェーズを完了にする場合は、未回答の検査コメントに対して返信することができます"
|
|
551
551
|
"(未回答の検査コメントに対して返信しないと、タスクを提出できないため)。"
|
|
552
552
|
"検査/受入フェーズを完了する場合は、未処置の検査コメントを対応完了/対応不要状態に変更できます"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: annofabcli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.109.0
|
|
4
4
|
Summary: Utility Command Line Interface for AnnoFab
|
|
5
5
|
Author: Kurusugawa Computer Inc.
|
|
6
6
|
License: MIT
|
|
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
18
|
Classifier: Topic :: Utilities
|
|
19
19
|
Requires-Python: >=3.9
|
|
20
|
-
Requires-Dist: annofabapi>=1.
|
|
20
|
+
Requires-Dist: annofabapi>=1.5.1
|
|
21
21
|
Requires-Dist: bokeh<3.7,>=3.3
|
|
22
22
|
Requires-Dist: dictdiffer
|
|
23
23
|
Requires-Dist: isodate
|