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.
@@ -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}: 未回答の検査コメントが {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.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.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