annofabcli 1.107.0__py3-none-any.whl → 1.108.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.
@@ -0,0 +1,308 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import enum
5
+ import logging
6
+ import multiprocessing
7
+ import sys
8
+ from dataclasses import dataclass
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 dataclasses_json import DataClassJsonMixin
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
+ ArgumentParser,
24
+ CommandLine,
25
+ CommandLineWithConfirm,
26
+ build_annofabapi_resource_and_login,
27
+ get_json_from_args,
28
+ )
29
+ from annofabcli.common.facade import AnnofabApiFacade
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class UpdateResult(Enum):
35
+ """更新結果の種類"""
36
+
37
+ SUCCESS = enum.auto()
38
+ """更新に成功した"""
39
+ SKIPPED = enum.auto()
40
+ """更新を実行しなかった(存在しないinput_data_id、ユーザー拒否等)"""
41
+ FAILED = enum.auto()
42
+ """更新を試みたが例外で失敗"""
43
+
44
+
45
+ @dataclass
46
+ class UpdatedInputData(DataClassJsonMixin):
47
+ """
48
+ 更新される入力データ
49
+ """
50
+
51
+ input_data_id: str
52
+ """更新対象の入力データを表すID"""
53
+ input_data_name: Optional[str] = None
54
+ """変更後の入力データ名(指定した場合のみ更新)"""
55
+ input_data_path: Optional[str] = None
56
+ """変更後の入力データパス(指定した場合のみ更新)"""
57
+
58
+
59
+ class UpdateInputDataMain(CommandLineWithConfirm):
60
+ def __init__(self, service: annofabapi.Resource, *, all_yes: bool = False) -> None:
61
+ self.service = service
62
+ CommandLineWithConfirm.__init__(self, all_yes)
63
+
64
+ def update_input_data(
65
+ self,
66
+ project_id: str,
67
+ input_data_id: str,
68
+ *,
69
+ new_input_data_name: Optional[str] = None,
70
+ new_input_data_path: Optional[str] = None,
71
+ input_data_index: Optional[int] = None,
72
+ ) -> UpdateResult:
73
+ """
74
+ 1個の入力データを更新します。
75
+ """
76
+ # ログメッセージの先頭の変数
77
+ log_prefix = f"input_data_id='{input_data_id}' :: "
78
+ if input_data_index is not None:
79
+ log_prefix = f"{input_data_index + 1}件目 :: {log_prefix}"
80
+
81
+ old_input_data = self.service.wrapper.get_input_data_or_none(project_id, input_data_id)
82
+ if old_input_data is None:
83
+ logger.warning(f"{log_prefix}入力データは存在しません。")
84
+ return UpdateResult.SKIPPED
85
+
86
+ # 更新する内容の確認メッセージを作成
87
+ changes = []
88
+ if new_input_data_name is not None:
89
+ changes.append(f"input_data_name='{old_input_data['input_data_name']}'を'{new_input_data_name}'に変更")
90
+ if new_input_data_path is not None:
91
+ changes.append(f"input_data_path='{old_input_data['input_data_path']}'を'{new_input_data_path}'に変更")
92
+
93
+ if len(changes) == 0:
94
+ logger.warning(f"{log_prefix}更新する内容が指定されていません。")
95
+ return UpdateResult.SKIPPED
96
+
97
+ change_message = "、".join(changes)
98
+ if not self.confirm_processing(f"{log_prefix}{change_message}しますか?"):
99
+ return UpdateResult.SKIPPED
100
+
101
+ request_body = old_input_data
102
+ request_body["last_updated_datetime"] = old_input_data["updated_datetime"]
103
+
104
+ if new_input_data_name is not None:
105
+ request_body["input_data_name"] = new_input_data_name
106
+ if new_input_data_path is not None:
107
+ request_body["input_data_path"] = new_input_data_path
108
+
109
+ self.service.api.put_input_data(project_id, input_data_id, request_body=request_body)
110
+ logger.debug(f"{log_prefix} :: 入力データを更新しました。 :: {changes}")
111
+ return UpdateResult.SUCCESS
112
+
113
+ def update_input_data_list_sequentially(
114
+ self,
115
+ project_id: str,
116
+ updated_input_data_list: list[UpdatedInputData],
117
+ ) -> None:
118
+ """複数の入力データを逐次的に更新します。"""
119
+ success_count = 0
120
+ skipped_count = 0 # 更新を実行しなかった個数
121
+ failed_count = 0 # 更新に失敗した個数
122
+
123
+ logger.info(f"{len(updated_input_data_list)} 件の入力データを更新します。")
124
+
125
+ for input_data_index, updated_input_data in enumerate(updated_input_data_list):
126
+ current_num = input_data_index + 1
127
+
128
+ # 進捗ログ出力
129
+ if current_num % 1000 == 0:
130
+ logger.info(f"{current_num} / {len(updated_input_data_list)} 件目の入力データを処理中...")
131
+
132
+ try:
133
+ result = self.update_input_data(
134
+ project_id,
135
+ updated_input_data.input_data_id,
136
+ new_input_data_name=updated_input_data.input_data_name,
137
+ new_input_data_path=updated_input_data.input_data_path,
138
+ input_data_index=input_data_index,
139
+ )
140
+ if result == UpdateResult.SUCCESS:
141
+ success_count += 1
142
+ elif result == UpdateResult.SKIPPED:
143
+ skipped_count += 1
144
+ except Exception:
145
+ logger.warning(f"{current_num}件目 :: input_data_id='{updated_input_data.input_data_id}'の入力データを更新するのに失敗しました。", exc_info=True)
146
+ failed_count += 1
147
+ continue
148
+
149
+ logger.info(f"{success_count} / {len(updated_input_data_list)} 件の入力データを更新しました。(成功: {success_count}件, スキップ: {skipped_count}件, 失敗: {failed_count}件)")
150
+
151
+ def _update_input_data_wrapper(self, args: tuple[int, UpdatedInputData], project_id: str) -> UpdateResult:
152
+ index, updated_input_data = args
153
+ try:
154
+ return self.update_input_data(
155
+ project_id,
156
+ input_data_id=updated_input_data.input_data_id,
157
+ new_input_data_name=updated_input_data.input_data_name,
158
+ new_input_data_path=updated_input_data.input_data_path,
159
+ input_data_index=index,
160
+ )
161
+ except Exception:
162
+ logger.warning(f"{index + 1}件目 :: input_data_id='{updated_input_data.input_data_id}'の入力データを更新するのに失敗しました。", exc_info=True)
163
+ return UpdateResult.FAILED
164
+
165
+ def update_input_data_list_in_parallel(
166
+ self,
167
+ project_id: str,
168
+ updated_input_data_list: list[UpdatedInputData],
169
+ parallelism: int,
170
+ ) -> None:
171
+ """複数の入力データを並列的に更新します。"""
172
+
173
+ logger.info(f"{len(updated_input_data_list)} 件の入力データを更新します。{parallelism}個のプロセスを使用して並列実行します。")
174
+
175
+ partial_func = partial(self._update_input_data_wrapper, project_id=project_id)
176
+ with multiprocessing.Pool(parallelism) as pool:
177
+ result_list = pool.map(partial_func, enumerate(updated_input_data_list))
178
+ success_count = len([e for e in result_list if e == UpdateResult.SUCCESS])
179
+ skipped_count = len([e for e in result_list if e == UpdateResult.SKIPPED])
180
+ failed_count = len([e for e in result_list if e == UpdateResult.FAILED])
181
+
182
+ logger.info(f"{success_count} / {len(updated_input_data_list)} 件の入力データを更新しました。(成功: {success_count}件, スキップ: {skipped_count}件, 失敗: {failed_count}件)")
183
+
184
+
185
+ def create_updated_input_data_list_from_dict(input_data_dict_list: list[dict[str, str]]) -> list[UpdatedInputData]:
186
+ return [UpdatedInputData.from_dict(e) for e in input_data_dict_list]
187
+
188
+
189
+ def create_updated_input_data_list_from_csv(csv_file: Path) -> list[UpdatedInputData]:
190
+ """入力データの情報が記載されているCSVを読み込み、UpdatedInputDataのlistを返します。
191
+ CSVには以下の列が存在します。
192
+ * input_data_id (必須)
193
+ * input_data_name (任意)
194
+ * input_data_path (任意)
195
+
196
+ Args:
197
+ csv_file (Path): CSVファイルのパス
198
+
199
+ Returns:
200
+ 更新対象の入力データのlist
201
+ """
202
+ df_input_data = pandas.read_csv(
203
+ csv_file,
204
+ # 文字列として読み込むようにする
205
+ dtype={"input_data_id": "string", "input_data_name": "string", "input_data_path": "string"},
206
+ )
207
+
208
+ input_data_dict_list = df_input_data.to_dict("records")
209
+ return [UpdatedInputData.from_dict(e) for e in input_data_dict_list]
210
+
211
+
212
+ CLI_COMMON_MESSAGE = "annofabcli input_data update: error:"
213
+
214
+
215
+ class UpdateInputData(CommandLine):
216
+ @staticmethod
217
+ def validate(args: argparse.Namespace) -> bool:
218
+ if args.parallelism is not None and not args.yes:
219
+ print( # noqa: T201
220
+ f"{CLI_COMMON_MESSAGE} argument --parallelism: '--parallelism'を指定するときは、必ず ``--yes`` を指定してください。",
221
+ file=sys.stderr,
222
+ )
223
+ return False
224
+
225
+ return True
226
+
227
+ def main(self) -> None:
228
+ args = self.args
229
+ if not self.validate(args):
230
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
231
+
232
+ main_obj = UpdateInputDataMain(self.service, all_yes=self.all_yes)
233
+
234
+ if args.csv is not None:
235
+ updated_input_data_list = create_updated_input_data_list_from_csv(args.csv)
236
+
237
+ elif args.json is not None:
238
+ input_data_dict_list = get_json_from_args(args.json)
239
+ if not isinstance(input_data_dict_list, list):
240
+ print(f"{CLI_COMMON_MESSAGE} JSON形式が不正です。オブジェクトの配列を指定してください。", file=sys.stderr) # noqa: T201
241
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
242
+ updated_input_data_list = create_updated_input_data_list_from_dict(input_data_dict_list)
243
+ else:
244
+ raise RuntimeError("argparse により相互排他が保証されているため、ここには到達しません")
245
+
246
+ project_id: str = args.project_id
247
+ if args.parallelism is not None:
248
+ main_obj.update_input_data_list_in_parallel(project_id, updated_input_data_list=updated_input_data_list, parallelism=args.parallelism)
249
+ else:
250
+ main_obj.update_input_data_list_sequentially(project_id, updated_input_data_list=updated_input_data_list)
251
+
252
+
253
+ def main(args: argparse.Namespace) -> None:
254
+ service = build_annofabapi_resource_and_login(args)
255
+ facade = AnnofabApiFacade(service)
256
+ UpdateInputData(service, facade, args).main()
257
+
258
+
259
+ def parse_args(parser: argparse.ArgumentParser) -> None:
260
+ argument_parser = ArgumentParser(parser)
261
+ argument_parser.add_project_id()
262
+
263
+ file_group = parser.add_mutually_exclusive_group(required=True)
264
+ file_group.add_argument(
265
+ "--csv",
266
+ type=Path,
267
+ help=(
268
+ "更新対象の入力データと更新後の値が記載されたCSVファイルのパスを指定します。\n"
269
+ "CSVのフォーマットは以下の通りです。"
270
+ "\n"
271
+ " * ヘッダ行あり, カンマ区切り\n"
272
+ " * input_data_id (required)\n"
273
+ " * input_data_name (optional)\n"
274
+ " * input_data_path (optional)\n"
275
+ "更新しないプロパティは、セルの値を空欄にしてください。\n"
276
+ ),
277
+ )
278
+
279
+ JSON_SAMPLE = '[{"input_data_id":"id1","input_data_name":"new_name1"},{"input_data_id":"id2","input_data_path":"new_path2"}]' # noqa: N806
280
+ file_group.add_argument(
281
+ "--json",
282
+ type=str,
283
+ help=(
284
+ "更新対象の入力データと更新後の値をJSON形式で指定します。\n"
285
+ "JSONの各キーは ``--csv`` に渡すCSVの各列に対応しています。\n"
286
+ "``file://`` を先頭に付けるとjsonファイルを指定できます。\n"
287
+ f"(ex) ``{JSON_SAMPLE}`` \n"
288
+ "更新しないプロパティは、キーを記載しないか値をnullにしてください。\n"
289
+ ),
290
+ )
291
+
292
+ parser.add_argument(
293
+ "--parallelism",
294
+ type=int,
295
+ choices=PARALLELISM_CHOICES,
296
+ help="使用するプロセス数(並列度)。指定しない場合は、逐次的に処理します。指定する場合は ``--yes`` も一緒に指定する必要があります。",
297
+ )
298
+
299
+ parser.set_defaults(subcommand_func=main)
300
+
301
+
302
+ def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
303
+ subcommand_name = "update"
304
+ subcommand_help = "入力データの名前または入力データのパスを更新します。"
305
+ epilog = "オーナロールを持つユーザで実行してください。"
306
+ parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, epilog=epilog)
307
+ parse_args(parser)
308
+ return parser
@@ -7,6 +7,7 @@ import annofabcli.project.copy_project
7
7
  import annofabcli.project.diff_projects
8
8
  import annofabcli.project.list_project
9
9
  import annofabcli.project.put_project
10
+ import annofabcli.project.update_configuration
10
11
  from annofabcli.common.cli import add_parser as common_add_parser
11
12
 
12
13
 
@@ -20,6 +21,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
20
21
  annofabcli.project.diff_projects.add_parser(subparsers)
21
22
  annofabcli.project.list_project.add_parser(subparsers)
22
23
  annofabcli.project.put_project.add_parser(subparsers)
24
+ annofabcli.project.update_configuration.add_parser(subparsers)
23
25
 
24
26
 
25
27
  def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
@@ -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
@@ -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}: 未回答の検査コメントが {unanswered_comment_count_for_task} 件あります。")
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}: 未回答の検査コメントに対する返信コメント('--reply_comment')が指定されていないので、スキップします。")
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}: 未回答の検査コメント {unanswered_comment_count_for_task} 件に対して、返信コメントを付与します。")
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}: 未処置の検査コメントが {unprocessed_inspection_count} 件あります。")
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}: 未処置の検査コメントに対する対応方法('--inspection_status')が指定されていないので、スキップします。")
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}: 未処置の検査コメント {unprocessed_inspection_count} 件を、{inspection_status.value} 状態にします。")
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}: task_id='{task_id}'のタスクは存在しないので、スキップします。")
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} : タスク情報 task_id='{task_id}', phase={task.phase.value}, phase_stage={task.phase_stage}, status={task.status.value}")
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}: {task.phase} フェーズを完了状態にするのに失敗しました。", exc_info=True)
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"タスク'{task_id}'のフェーズを完了状態にするのに失敗しました。", exc_info=True)
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"タスク'{task_id}'のフェーズを完了状態にするのに失敗しました。", exc_info=True)
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.107.0
3
+ Version: 1.108.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.4.7
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