annofabcli 1.106.6__py3-none-any.whl → 1.106.8__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.
@@ -46,7 +46,7 @@ class ChangeAnnotationAttributesMain(CommandLineWithConfirm):
46
46
  service: annofabapi.Resource,
47
47
  *,
48
48
  project_id: str,
49
- is_force: bool,
49
+ include_completed: bool,
50
50
  all_yes: bool,
51
51
  ) -> None:
52
52
  self.service = service
@@ -54,7 +54,7 @@ class ChangeAnnotationAttributesMain(CommandLineWithConfirm):
54
54
  CommandLineWithConfirm.__init__(self, all_yes)
55
55
 
56
56
  self.project_id = project_id
57
- self.is_force = is_force
57
+ self.include_completed = include_completed
58
58
 
59
59
  self.dump_annotation_obj = DumpAnnotationMain(service, project_id)
60
60
 
@@ -114,9 +114,9 @@ class ChangeAnnotationAttributesMain(CommandLineWithConfirm):
114
114
  *,
115
115
  backup_dir: Optional[Path] = None,
116
116
  task_index: Optional[int] = None,
117
- ) -> bool:
117
+ ) -> tuple[bool, int]:
118
118
  """
119
- タスクに対してアノテーション属性を変更する。
119
+ タスクに対してアノテーション属性値を変更する。
120
120
 
121
121
  Args:
122
122
  project_id:
@@ -127,23 +127,24 @@ class ChangeAnnotationAttributesMain(CommandLineWithConfirm):
127
127
  backup_dir: アノテーションをバックアップとして保存するディレクトリ。指定しない場合は、バックアップを取得しない。
128
128
 
129
129
  Returns:
130
- アノテーションの属性を変更するAPI ``change_annotation_attributes`` を実行したか否か
130
+ tuple[0]: 成功した場合はTrue、失敗した場合はFalse
131
+ tuple[1]: 変更したアノテーションの個数
131
132
  """
132
133
  logger_prefix = f"{task_index + 1!s} 件目: " if task_index is not None else ""
133
134
  dict_task = self.service.wrapper.get_task_or_none(self.project_id, task_id)
134
135
  if dict_task is None:
135
136
  logger.warning(f"task_id = '{task_id}' は存在しません。")
136
- return False
137
+ return False, 0
137
138
 
138
139
  task: Task = Task.from_dict(dict_task)
139
140
  if task.status == TaskStatus.WORKING:
140
141
  logger.warning(f"task_id='{task_id}': タスクが作業中状態のため、スキップします。")
141
- return False
142
+ return False, 0
142
143
 
143
- if not self.is_force: # noqa: SIM102
144
+ if not self.include_completed: # noqa: SIM102
144
145
  if task.status == TaskStatus.COMPLETE:
145
- logger.warning(f"task_id='{task_id}': タスクが完了状態のため、スキップします。")
146
- return False
146
+ logger.warning(f"task_id='{task_id}': タスクが完了状態のため、スキップします。完了状態のタスクのアノテーション属性値を変更するには、 ``--include_completed`` を指定してください。")
147
+ return False, 0
147
148
 
148
149
  annotation_list = self.get_annotation_list_for_task(task_id, annotation_query)
149
150
  logger.info(
@@ -151,17 +152,17 @@ class ChangeAnnotationAttributesMain(CommandLineWithConfirm):
151
152
  )
152
153
  if len(annotation_list) == 0:
153
154
  logger.info(f"{logger_prefix}task_id='{task_id}'には変更対象のアノテーションが存在しないので、スキップします。")
154
- return False
155
+ return False, 0
155
156
 
156
- if not self.confirm_processing(f"task_id='{task_id}' のアノテーション属性を変更しますか?"):
157
- return False
157
+ if not self.confirm_processing(f"task_id='{task_id}' のアノテーション属性値を変更しますか?"):
158
+ return False, 0
158
159
 
159
160
  if backup_dir is not None:
160
161
  self.dump_annotation_obj.dump_annotation_for_task(task_id, output_dir=backup_dir)
161
162
 
162
163
  self.change_annotation_attributes(annotation_list, additional_data_list)
163
164
  logger.info(f"{logger_prefix}task_id='{task_id}': {len(annotation_list)} 個のアノテーションの属性値を変更しました。")
164
- return True
165
+ return True, len(annotation_list)
165
166
 
166
167
  def change_attributes_for_task_wrapper(
167
168
  self,
@@ -170,7 +171,7 @@ class ChangeAnnotationAttributesMain(CommandLineWithConfirm):
170
171
  additional_data_list: list[dict[str, Any]],
171
172
  *,
172
173
  backup_dir: Optional[Path] = None,
173
- ) -> bool:
174
+ ) -> tuple[bool, int]:
174
175
  task_index, task_id = tpl
175
176
  try:
176
177
  return self.change_attributes_for_task(
@@ -181,8 +182,8 @@ class ChangeAnnotationAttributesMain(CommandLineWithConfirm):
181
182
  task_index=task_index,
182
183
  )
183
184
  except Exception: # pylint: disable=broad-except
184
- logger.warning(f"タスク'{task_id}'のアノテーションの属性の変更に失敗しました。", exc_info=True)
185
- return False
185
+ logger.warning(f"タスク'{task_id}'のアノテーションの属性値の変更に失敗しました。", exc_info=True)
186
+ return False, 0
186
187
 
187
188
  def change_annotation_attributes_for_task_list(
188
189
  self,
@@ -209,8 +210,9 @@ class ChangeAnnotationAttributesMain(CommandLineWithConfirm):
209
210
 
210
211
  if backup_dir is not None:
211
212
  backup_dir.mkdir(exist_ok=True, parents=True)
212
-
213
213
  success_count = 0
214
+ # 変更したアノテーションの個数
215
+ changed_annotation_count = 0
214
216
  if parallelism is not None:
215
217
  func = functools.partial(
216
218
  self.change_attributes_for_task_wrapper,
@@ -219,26 +221,28 @@ class ChangeAnnotationAttributesMain(CommandLineWithConfirm):
219
221
  backup_dir=backup_dir,
220
222
  )
221
223
  with multiprocessing.Pool(parallelism) as pool:
222
- result_bool_list = pool.map(func, enumerate(task_id_list))
223
- success_count = len([e for e in result_bool_list if e])
224
+ result_tuple_list = pool.map(func, enumerate(task_id_list))
225
+ success_count = len([e for e in result_tuple_list if e[0]])
226
+ changed_annotation_count = sum(e[1] for e in result_tuple_list)
224
227
 
225
228
  else:
226
229
  for task_index, task_id in enumerate(task_id_list):
227
230
  try:
228
- result = self.change_attributes_for_task(
231
+ result, sub_changed_annotation_count = self.change_attributes_for_task(
229
232
  task_id,
230
233
  annotation_query=annotation_query,
231
234
  additional_data_list=additional_data_list,
232
235
  backup_dir=backup_dir,
233
236
  task_index=task_index,
234
237
  )
238
+ changed_annotation_count += sub_changed_annotation_count
235
239
  if result:
236
240
  success_count += 1
237
241
  except Exception:
238
- logger.warning(f"タスク'{task_id}'のアノテーションの属性の変更に失敗しました。", exc_info=True)
242
+ logger.warning(f"タスク'{task_id}'のアノテーションの属性値の変更に失敗しました。", exc_info=True)
239
243
  continue
240
244
 
241
- logger.info(f"{success_count} / {len(task_id_list)} 件のタスクに対してアノテーションの属性を変更しました。")
245
+ logger.info(f"{success_count} / {len(task_id_list)} 件のタスクに対して {changed_annotation_count} 件のアノテーションの属性値を変更しました。")
242
246
 
243
247
 
244
248
  class ChangeAttributesOfAnnotation(CommandLine):
@@ -310,15 +314,15 @@ class ChangeAttributesOfAnnotation(CommandLine):
310
314
  backup_dir = Path(args.backup)
311
315
 
312
316
  super().validate_project(project_id, [ProjectMemberRole.OWNER, ProjectMemberRole.ACCEPTER])
313
- if args.force: # noqa: SIM102
317
+ if args.include_completed: # noqa: SIM102
314
318
  if not self.facade.contains_any_project_member_role(project_id, [ProjectMemberRole.OWNER]):
315
319
  print( # noqa: T201
316
- f"{self.COMMON_MESSAGE} argument --force : '--force' 引数を利用するにはプロジェクトのオーナーロールを持つユーザーで実行する必要があります。",
320
+ f"{self.COMMON_MESSAGE} argument --include_completed : '--include_completed' 引数を利用するにはプロジェクトのオーナーロールを持つユーザーで実行する必要があります。",
317
321
  file=sys.stderr,
318
322
  )
319
323
  sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
320
324
 
321
- main_obj = ChangeAnnotationAttributesMain(self.service, project_id=project_id, is_force=args.force, all_yes=args.yes)
325
+ main_obj = ChangeAnnotationAttributesMain(self.service, project_id=project_id, include_completed=args.include_completed, all_yes=args.yes)
322
326
  main_obj.change_annotation_attributes_for_task_list(
323
327
  task_id_list,
324
328
  annotation_query=annotation_query,
@@ -358,7 +362,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
358
362
  )
359
363
 
360
364
  parser.add_argument(
361
- "--force",
365
+ "--include_completed",
362
366
  action="store_true",
363
367
  help="完了状態のタスクのアノテーション属性も変更します。ただし、オーナーロールを持つユーザーでしか実行できません。",
364
368
  )
@@ -381,10 +385,11 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
381
385
 
382
386
  def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
383
387
  subcommand_name = "change_attributes"
384
- subcommand_help = "アノテーションの属性を変更します。"
388
+ subcommand_help = "アノテーションの属性値を変更します。"
385
389
  description = (
386
- "アノテーションの属性を一括で変更します。ただし、作業中状態のタスクのアノテーションの属性は変更できません。"
387
- "間違えてアノテーション属性を変更したときに復元できるようにするため、 ``--backup`` でバックアップ用のディレクトリを指定することを推奨します。"
390
+ "アノテーションの属性値を一括で変更します。ただし、作業中状態のタスクに含まれるアノテーションは変更できません。"
391
+ "完了状態のタスクに含まれるアノテーションは、デフォルトでは変更できません。"
392
+ "間違えてアノテーション属性値を変更したときに復元できるようにするため、 ``--backup`` でバックアップ用のディレクトリを指定することを推奨します。"
388
393
  )
389
394
  epilog = "オーナロールまたはチェッカーロールを持つユーザで実行してください。"
390
395
 
@@ -6,7 +6,7 @@ import logging
6
6
  import sys
7
7
  from collections import defaultdict
8
8
  from pathlib import Path
9
- from typing import Any, Optional, Union
9
+ from typing import Optional, Union
10
10
 
11
11
  import annofabapi
12
12
  import pandas
@@ -76,7 +76,7 @@ class ChangeAnnotationAttributesPerAnnotationMain(CommandLineWithConfirm):
76
76
  self.dump_annotation_obj = DumpAnnotationMain(service, project_id)
77
77
  super().__init__(all_yes)
78
78
 
79
- def change_annotation_attributes_by_frame(self, task_id: str, input_data_id: str, anno_list: list[TargetAnnotation]) -> bool:
79
+ def change_annotation_attributes_by_frame(self, task_id: str, input_data_id: str, anno_list: list[TargetAnnotation]) -> tuple[int, int]:
80
80
  """
81
81
  フレームごとにアノテーション属性値を変更する。
82
82
 
@@ -85,6 +85,10 @@ class ChangeAnnotationAttributesPerAnnotationMain(CommandLineWithConfirm):
85
85
  input_data_id: 入力データID
86
86
  additional_data_list: 変更後の属性値(`AdditionalDataListV2`スキーマ)
87
87
 
88
+ Returns:
89
+ [1]: 属性値の変更に成功したアノテーション数
90
+ [2]: 属性値を変更できなかった(変更対象のアノテーションが存在しなかった)アノテーション数
91
+
88
92
  """
89
93
  editor_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id=task_id, input_data_id=input_data_id, query_params={"v": "2"})
90
94
 
@@ -94,26 +98,47 @@ class ChangeAnnotationAttributesPerAnnotationMain(CommandLineWithConfirm):
94
98
 
95
99
  details_map = {detail["annotation_id"]: detail for detail in editor_annotation["details"]}
96
100
 
97
- def _to_request_body_elm(anno: TargetAnnotation) -> dict[str, Any]:
101
+ request_body = []
102
+ non_target_annotation_count = 0
103
+ for anno in anno_list:
104
+ if anno.annotation_id not in details_map:
105
+ logger.warning(
106
+ f"task_id='{task_id}', input_data_id='{input_data_id}' :: "
107
+ f"annotation_id='{anno.annotation_id}'であるアノテーションが存在しないため、"
108
+ "このアノテーションの属性値の変更をスキップします。"
109
+ )
110
+ non_target_annotation_count += 1
111
+ continue
112
+
113
+ label_id = details_map[anno.annotation_id]["label_id"]
98
114
  additional_data_list = convert_attributes_from_cli_to_additional_data_list_v2(anno.attributes, annotation_specs=self.annotation_specs)
99
- return {
100
- "data": {
101
- "project_id": editor_annotation["project_id"],
102
- "task_id": editor_annotation["task_id"],
103
- "input_data_id": editor_annotation["input_data_id"],
104
- "updated_datetime": editor_annotation["updated_datetime"],
105
- "annotation_id": anno.annotation_id,
106
- "label_id": details_map[anno.annotation_id]["label_id"],
107
- "additional_data_list": additional_data_list,
108
- },
109
- "_type": "PutV2",
110
- }
111
-
112
- request_body = [_to_request_body_elm(annotation) for annotation in anno_list]
113
-
114
- self.service.api.batch_update_annotations(self.project_id, request_body=request_body)
115
- logger.debug(f"task_id='{task_id}', input_data_id='{input_data_id}' :: {len(request_body)}件の属性値を変更しました。")
116
- return True
115
+ request_body.append(
116
+ {
117
+ "data": {
118
+ "project_id": editor_annotation["project_id"],
119
+ "task_id": editor_annotation["task_id"],
120
+ "input_data_id": editor_annotation["input_data_id"],
121
+ "updated_datetime": editor_annotation["updated_datetime"],
122
+ "annotation_id": anno.annotation_id,
123
+ "label_id": label_id,
124
+ "additional_data_list": additional_data_list,
125
+ },
126
+ "_type": "PutV2",
127
+ }
128
+ )
129
+
130
+ if request_body:
131
+ self.service.api.batch_update_annotations(self.project_id, request_body=request_body)
132
+ else:
133
+ logger.debug(f"task_id='{task_id}', input_data_id='{input_data_id}' :: 変更対象のアノテーションがありませんでした。")
134
+
135
+ succeed_to_change_annotation_count = len(request_body)
136
+ logger.debug(
137
+ f"task_id='{task_id}', input_data_id='{input_data_id}' :: "
138
+ f"{succeed_to_change_annotation_count}/{len(anno_list)}件の属性値を変更しました。"
139
+ f"{non_target_annotation_count}件のアノテーションは存在しなかったため、属性値の変更をスキップしました。"
140
+ )
141
+ return succeed_to_change_annotation_count, non_target_annotation_count
117
142
 
118
143
  def change_annotation_attributes_for_task(self, task_id: str, annotation_list_per_input_data_id: dict[str, list[TargetAnnotation]]) -> tuple[bool, int, int]:
119
144
  """
@@ -159,10 +184,9 @@ class ChangeAnnotationAttributesPerAnnotationMain(CommandLineWithConfirm):
159
184
 
160
185
  for input_data_id, sub_anno_list in annotation_list_per_input_data_id.items():
161
186
  try:
162
- if self.change_annotation_attributes_by_frame(task_id, input_data_id, sub_anno_list):
163
- succeed_to_change_annotation_count += len(sub_anno_list)
164
- else:
165
- failed_to_change_annotation_count += len(sub_anno_list)
187
+ tmp_succeed_to_change_annotation_count, tmp_failed_to_change_annotation_count = self.change_annotation_attributes_by_frame(task_id, input_data_id, sub_anno_list)
188
+ succeed_to_change_annotation_count += tmp_succeed_to_change_annotation_count
189
+ failed_to_change_annotation_count += tmp_failed_to_change_annotation_count
166
190
  except Exception:
167
191
  logger.warning(f"task_id='{task_id}', input_data_id='{input_data_id}' :: アノテーションの属性値変更に失敗しました。", exc_info=True)
168
192
  failed_to_change_annotation_count += len(sub_anno_list)
@@ -9,7 +9,6 @@ import uuid
9
9
  import zipfile
10
10
  from collections.abc import Iterator
11
11
  from dataclasses import dataclass
12
- from functools import partial
13
12
  from pathlib import Path
14
13
  from typing import Any, Optional, Union
15
14
 
@@ -408,6 +407,7 @@ class ImportAnnotationMain(CommandLineWithConfirm):
408
407
  is_force: bool,
409
408
  is_merge: bool,
410
409
  is_overwrite: bool,
410
+ converter: AnnotationConverter,
411
411
  ) -> None:
412
412
  self.service = service
413
413
  self.facade = AnnofabApiFacade(service)
@@ -417,8 +417,9 @@ class ImportAnnotationMain(CommandLineWithConfirm):
417
417
  self.is_force = is_force
418
418
  self.is_merge = is_merge
419
419
  self.is_overwrite = is_overwrite
420
+ self.converter = converter
420
421
 
421
- def put_annotation_for_input_data(self, parser: SimpleAnnotationParser, converter: AnnotationConverter) -> bool:
422
+ def put_annotation_for_input_data(self, parser: SimpleAnnotationParser) -> bool:
422
423
  task_id = parser.task_id
423
424
  input_data_id = parser.input_data_id
424
425
 
@@ -444,18 +445,18 @@ class ImportAnnotationMain(CommandLineWithConfirm):
444
445
 
445
446
  logger.info(f"task_id='{task_id}', input_data_id='{input_data_id}' :: {len(simple_annotation.details)} 件のアノテーションを登録します。")
446
447
  if self.is_merge:
447
- request_body = converter.convert_annotation_details(parser, simple_annotation.details, old_details=old_annotation["details"], updated_datetime=old_annotation["updated_datetime"])
448
+ request_body = self.converter.convert_annotation_details(parser, simple_annotation.details, old_details=old_annotation["details"], updated_datetime=old_annotation["updated_datetime"])
448
449
  else:
449
- request_body = converter.convert_annotation_details(parser, simple_annotation.details, old_details=[], updated_datetime=old_annotation["updated_datetime"])
450
+ request_body = self.converter.convert_annotation_details(parser, simple_annotation.details, old_details=[], updated_datetime=old_annotation["updated_datetime"])
450
451
 
451
452
  self.service.api.put_annotation(self.project_id, task_id, input_data_id, request_body=request_body, query_params={"v": "2"})
452
453
  return True
453
454
 
454
- def put_annotation_for_task(self, task_parser: SimpleAnnotationParserByTask, converter: AnnotationConverter) -> int:
455
+ def put_annotation_for_task(self, task_parser: SimpleAnnotationParserByTask) -> int:
455
456
  success_count = 0
456
457
  for parser in task_parser.lazy_parse():
457
458
  try:
458
- if self.put_annotation_for_input_data(parser, converter):
459
+ if self.put_annotation_for_input_data(parser):
459
460
  success_count += 1
460
461
  except Exception: # pylint: disable=broad-except
461
462
  logger.warning(
@@ -465,7 +466,7 @@ class ImportAnnotationMain(CommandLineWithConfirm):
465
466
 
466
467
  return success_count
467
468
 
468
- def execute_task(self, task_parser: SimpleAnnotationParserByTask, converter: AnnotationConverter, task_index: Optional[int] = None) -> bool:
469
+ def execute_task(self, task_parser: SimpleAnnotationParserByTask, task_index: Optional[int] = None) -> bool:
469
470
  """
470
471
  1個のタスクに対してアノテーションを登録する。
471
472
 
@@ -514,7 +515,7 @@ class ImportAnnotationMain(CommandLineWithConfirm):
514
515
  )
515
516
  return False
516
517
 
517
- result_count = self.put_annotation_for_task(task_parser, converter)
518
+ result_count = self.put_annotation_for_task(task_parser)
518
519
  logger.info(f"{logger_prefix}タスク'{task_parser.task_id}'の入力データ {result_count} 個に対してアノテーションをインポートしました。")
519
520
 
520
521
  if changed_operator:
@@ -531,11 +532,10 @@ class ImportAnnotationMain(CommandLineWithConfirm):
531
532
  def execute_task_wrapper(
532
533
  self,
533
534
  tpl: tuple[int, SimpleAnnotationParserByTask],
534
- converter: AnnotationConverter,
535
535
  ) -> bool:
536
536
  task_index, task_parser = tpl
537
537
  try:
538
- return self.execute_task(task_parser, converter=converter, task_index=task_index)
538
+ return self.execute_task(task_parser, task_index=task_index)
539
539
  except Exception: # pylint: disable=broad-except
540
540
  logger.warning(f"task_id='{task_parser.task_id}' のアノテーションのインポートに失敗しました。", exc_info=True)
541
541
  return False
@@ -543,18 +543,9 @@ class ImportAnnotationMain(CommandLineWithConfirm):
543
543
  def main(
544
544
  self,
545
545
  iter_task_parser: Iterator[SimpleAnnotationParserByTask],
546
- converter: AnnotationConverter,
547
546
  target_task_ids: Optional[set[str]] = None,
548
547
  parallelism: Optional[int] = None,
549
548
  ) -> None:
550
- """
551
- アノテーションのインポート処理を実行するメイン関数です。
552
-
553
- Notes:
554
- `converter`をインスタンス変数でなく引数として渡している理由:
555
- `multiprocessing.Pool`でシリアライズ化する際、"TypeError: cannot pickle '_thread.RLock' object"というエラーが発生するため
556
- """
557
-
558
549
  def get_iter_task_parser_from_task_ids(_iter_task_parser: Iterator[SimpleAnnotationParserByTask], _target_task_ids: set[str]) -> Iterator[SimpleAnnotationParserByTask]:
559
550
  for task_parser in _iter_task_parser:
560
551
  if task_parser.task_id in _target_task_ids:
@@ -571,15 +562,14 @@ class ImportAnnotationMain(CommandLineWithConfirm):
571
562
  task_count = 0
572
563
  if parallelism is not None:
573
564
  with multiprocessing.Pool(parallelism) as pool:
574
- func = partial(self.execute_task_wrapper, converter=converter)
575
- result_bool_list = pool.map(func, enumerate(iter_task_parser))
565
+ result_bool_list = pool.map(self.execute_task_wrapper, enumerate(iter_task_parser))
576
566
  success_count = len([e for e in result_bool_list if e])
577
567
  task_count = len(result_bool_list)
578
568
 
579
569
  else:
580
570
  for task_index, task_parser in enumerate(iter_task_parser):
581
571
  try:
582
- result = self.execute_task(task_parser, converter=converter, task_index=task_index)
572
+ result = self.execute_task(task_parser, task_index=task_index)
583
573
  if result:
584
574
  success_count += 1
585
575
  except Exception:
@@ -618,6 +608,13 @@ class ImportAnnotation(CommandLine):
618
608
  print(f"{COMMON_MESSAGE} argument --annotation: ZIPファイルまたはディレクトリを指定してください。", file=sys.stderr) # noqa: T201
619
609
  return False
620
610
 
611
+ if args.parallelism is not None and annotation_path.is_file() and zipfile.is_zipfile(annotation_path):
612
+ print( # noqa: T201
613
+ f"{COMMON_MESSAGE} argument --parallelism: '--annotation'にZIPファイルを指定した場合は、'--parallelism'を指定できません。",
614
+ file=sys.stderr,
615
+ )
616
+ return False
617
+
621
618
  if args.parallelism is not None and not args.yes:
622
619
  print( # noqa: T201
623
620
  f"{COMMON_MESSAGE} argument --parallelism: '--parallelism'を指定するときは、必ず '--yes' を指定してください。",
@@ -642,7 +639,7 @@ class ImportAnnotation(CommandLine):
642
639
  # Simpleアノテーションの読み込み
643
640
  if annotation_path.is_dir():
644
641
  iter_task_parser = lazy_parse_simple_annotation_dir_by_task(annotation_path)
645
- elif zipfile.is_zipfile(str(annotation_path)):
642
+ elif zipfile.is_zipfile(annotation_path):
646
643
  iter_task_parser = lazy_parse_simple_annotation_zip_by_task(annotation_path)
647
644
  else:
648
645
  logger.warning(f"annotation_path: '{annotation_path}' は、zipファイルまたはディレクトリではありませんでした。")
@@ -659,9 +656,10 @@ class ImportAnnotation(CommandLine):
659
656
  is_merge=args.merge,
660
657
  is_overwrite=args.overwrite,
661
658
  is_force=args.force,
659
+ converter=converter,
662
660
  )
663
661
 
664
- main_obj.main(iter_task_parser, target_task_ids=target_task_ids, converter=converter, parallelism=args.parallelism)
662
+ main_obj.main(iter_task_parser, target_task_ids=target_task_ids, parallelism=args.parallelism)
665
663
 
666
664
 
667
665
  def main(args: argparse.Namespace) -> None:
@@ -717,7 +715,9 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
717
715
  "--parallelism",
718
716
  type=int,
719
717
  choices=PARALLELISM_CHOICES,
720
- help="並列度。指定しない場合は、逐次的に処理します。指定した場合は、``--yes`` も指定してください。",
718
+ help="並列度。指定しない場合は、逐次的に処理します。"
719
+ "ただし ``--annotation`` にZIPファイルを指定した場合は、``--parallelism`` を指定できません。"
720
+ "また、``--parallelism`` を指定した場合は、``--yes`` も指定してください。",
721
721
  )
722
722
 
723
723
  parser.set_defaults(subcommand_func=main)
@@ -26,10 +26,11 @@ logger = logging.getLogger(__name__)
26
26
 
27
27
 
28
28
  class ChangeOperatorMain:
29
- def __init__(self, service: annofabapi.Resource, all_yes: bool) -> None: # noqa: FBT001
29
+ def __init__(self, service: annofabapi.Resource, *, all_yes: bool, include_on_hold: bool = False) -> None:
30
30
  self.service = service
31
31
  self.facade = AnnofabApiFacade(service)
32
32
  self.all_yes = all_yes
33
+ self.include_on_hold = include_on_hold
33
34
 
34
35
  def confirm_processing(self, confirm_message: str) -> bool:
35
36
  """
@@ -55,10 +56,10 @@ class ChangeOperatorMain:
55
56
  return yes
56
57
 
57
58
  def confirm_change_operator(self, task: Task) -> bool:
58
- confirm_message = f"task_id = {task.task_id} のタスクの担当者を変更しますか?"
59
+ confirm_message = f"task_id='{task.task_id}' のタスクの担当者を変更しますか?"
59
60
  return self.confirm_processing(confirm_message)
60
61
 
61
- def change_operator_for_task(
62
+ def change_operator_for_task( # noqa: PLR0911
62
63
  self,
63
64
  project_id: str,
64
65
  task_id: str,
@@ -69,7 +70,7 @@ class ChangeOperatorMain:
69
70
  logging_prefix = f"{task_index + 1} 件目" if task_index is not None else ""
70
71
  dict_task = self.service.wrapper.get_task_or_none(project_id, task_id)
71
72
  if dict_task is None:
72
- logger.warning(f"{logging_prefix}: task_id='{task_id}'のタスクは存在しないので、スキップします。")
73
+ logger.warning(f"{logging_prefix} :: task_id='{task_id}'のタスクは存在しないので、スキップします。")
73
74
  return False
74
75
 
75
76
  task: Task = Task.from_dict(dict_task)
@@ -78,14 +79,23 @@ class ChangeOperatorMain:
78
79
  if task.account_id is not None:
79
80
  now_user_id = self.facade.get_user_id_from_account_id(project_id, task.account_id)
80
81
 
81
- logger.debug(f"{logging_prefix} : task_id = {task.task_id}, status = {task.status.value}, phase = {task.phase.value}, phase_stage = {task.phase_stage}, user_id = {now_user_id}")
82
+ logger.debug(f"{logging_prefix} :: task_id='{task.task_id}', status='{task.status.value}', phase='{task.phase.value}', phase_stage='{task.phase_stage}', user_id='{now_user_id}'")
83
+ if task.account_id == new_account_id:
84
+ logger.info(f"{logging_prefix} :: task_id='{task_id}' :: タスクの担当者はすでにuser_id='{now_user_id}'のユーザーです。担当者を変更する必要がないのでスキップします。")
85
+ return False
82
86
 
83
87
  if task.status in [TaskStatus.COMPLETE, TaskStatus.WORKING]:
84
- logger.warning(f"{logging_prefix} : task_id = {task_id} : タスクのstatusがworking or complete なので、担当者を変更できません。")
88
+ logger.warning(f"{logging_prefix} :: task_id='{task_id}' :: タスクが作業中状態または完了状態なので、担当者を変更できません。 :: status='{task.status.value}'")
89
+ return False
90
+
91
+ if task.status == TaskStatus.ON_HOLD and not self.include_on_hold:
92
+ logger.warning(
93
+ f"{logging_prefix} :: task_id='{task_id}' :: タスクが保留中状態なので、担当者を変更できません。保留中状態のタスクの担当者も変更する場合は、'--include_on_hold'を指定してください。"
94
+ )
85
95
  return False
86
96
 
87
97
  if not match_task_with_query(task, task_query):
88
- logger.debug(f"{logging_prefix} : task_id = {task_id} : `--task_query` の条件にマッチしないため、スキップします。task_query={task_query}")
98
+ logger.debug(f"{logging_prefix} :: task_id='{task_id}' :: `--task_query` の条件にマッチしないため、スキップします。task_query='{task_query}'")
89
99
  return False
90
100
 
91
101
  if not self.confirm_change_operator(task):
@@ -94,11 +104,11 @@ class ChangeOperatorMain:
94
104
  try:
95
105
  # 担当者を変更する
96
106
  self.service.wrapper.change_task_operator(project_id, task_id, operator_account_id=new_account_id)
97
- logger.debug(f"{logging_prefix} : task_id = {task_id}, phase={dict_task['phase']} のタスクの担当者を変更しました。")
107
+ logger.debug(f"{logging_prefix} :: task_id='{task_id}'であるタスクの担当者を変更しました。 :: phase='{dict_task['phase']}'")
98
108
  return True # noqa: TRY300
99
109
 
100
110
  except requests.exceptions.HTTPError:
101
- logger.warning(f"{logging_prefix} : task_id = {task_id} の担当者を変更するのに失敗しました。", exc_info=True)
111
+ logger.warning(f"{logging_prefix} :: task_id='{task_id}'である担当者を変更するのに失敗しました。", exc_info=True)
102
112
  return False
103
113
 
104
114
  def change_operator_for_task_wrapper(
@@ -118,19 +128,21 @@ class ChangeOperatorMain:
118
128
  new_account_id=new_account_id,
119
129
  )
120
130
  except Exception: # pylint: disable=broad-except
121
- logger.warning(f"タスク'{task_id}'の担当者の変更に失敗しました。", exc_info=True)
131
+ logger.warning(f"task_id='{task_id}'であるタスク担当者の変更に失敗しました。", exc_info=True)
122
132
  return False
123
133
 
124
134
  def change_operator(
125
135
  self,
126
136
  project_id: str,
127
137
  task_id_list: list[str],
138
+ *,
128
139
  new_user_id: Optional[str] = None,
129
140
  task_query: Optional[TaskQuery] = None,
130
141
  parallelism: Optional[int] = None,
131
142
  ) -> None:
132
143
  """
133
- 検査コメントを付与して、タスクを差し戻す
144
+ 指定した複数のタスクの担当者を変更します。
145
+
134
146
  Args:
135
147
  project_id:
136
148
  task_id_list:
@@ -143,13 +155,13 @@ class ChangeOperatorMain:
143
155
  if new_user_id is not None:
144
156
  new_account_id = self.facade.get_account_id_from_user_id(project_id, new_user_id)
145
157
  if new_account_id is None:
146
- logger.error(f"ユーザ '{new_user_id}' のaccount_idが見つかりませんでした。終了します。")
158
+ logger.error(f"user_id='{new_user_id}'であるユーザーは、project_id='{project_id}'のプロジェクトのメンバーではありません。終了します。")
147
159
  return
148
160
  else:
149
- logger.info(f"{len(task_id_list)} 件のタスクの担当者を、{new_user_id}に変更します。")
161
+ logger.info(f"{len(task_id_list)} 件のタスクの担当者を、user_id='{new_user_id}'のユーザーに変更します。")
150
162
  else:
151
163
  new_account_id = None
152
- logger.info(f"{len(task_id_list)} 件のタスクの担当者を未割り当てに変更します。")
164
+ logger.info(f"{len(task_id_list)} 件のタスクの担当者を「未割り当て」に変更します。")
153
165
 
154
166
  success_count = 0
155
167
 
@@ -172,7 +184,7 @@ class ChangeOperatorMain:
172
184
  if result:
173
185
  success_count += 1
174
186
  except Exception: # pylint: disable=broad-except
175
- logger.warning(f"タスク'{task_id}'の担当者の変更に失敗しました。", exc_info=True)
187
+ logger.warning(f"task_id='{task_id}'であるタスクの担当者の変更に失敗しました。", exc_info=True)
176
188
  continue
177
189
 
178
190
  logger.info(f"{success_count} / {len(task_id_list)} 件 タスクの担当者を変更しました。")
@@ -212,7 +224,7 @@ class ChangeOperator(CommandLine):
212
224
  project_id = args.project_id
213
225
  super().validate_project(project_id, [ProjectMemberRole.OWNER, ProjectMemberRole.ACCEPTER])
214
226
 
215
- main_obj = ChangeOperatorMain(self.service, all_yes=self.all_yes)
227
+ main_obj = ChangeOperatorMain(self.service, all_yes=self.all_yes, include_on_hold=args.include_on_hold)
216
228
  main_obj.change_operator(
217
229
  project_id,
218
230
  task_id_list=task_id_list,
@@ -242,6 +254,12 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
242
254
 
243
255
  argument_parser.add_task_query()
244
256
 
257
+ parser.add_argument(
258
+ "--include_on_hold",
259
+ action="store_true",
260
+ help="指定した場合、保留中のタスクの担当者も変更します。指定しない場合、保留中のタスクはスキップされます。",
261
+ )
262
+
245
263
  parser.add_argument(
246
264
  "--parallelism",
247
265
  type=int,
@@ -255,7 +273,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
255
273
  def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
256
274
  subcommand_name = "change_operator"
257
275
  subcommand_help = "タスクの担当者を変更します。"
258
- description = "タスクの担当者を変更します。ただし、作業中また完了状態のタスクは、担当者を変更できません。"
276
+ description = "タスクの担当者を変更します。作業中状態、完了状態のタスクは、担当者を変更できません。保留中状態のタスクは、デフォルトでは担当者を変更できません。"
259
277
  epilog = "チェッカーまたはオーナロールを持つユーザで実行してください。"
260
278
 
261
279
  parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: annofabcli
3
- Version: 1.106.6
3
+ Version: 1.106.8
4
4
  Summary: Utility Command Line Interface for AnnoFab
5
5
  Author: Kurusugawa Computer Inc.
6
6
  License: MIT
@@ -3,14 +3,14 @@ annofabcli/__main__.py,sha256=YfuJE9E43xSo6iHTxVuQPHCz2eBaJS07QnVU42-0znQ,5293
3
3
  annofabcli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  annofabcli/annotation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  annofabcli/annotation/annotation_query.py,sha256=VwfPWpLOpVa2SeEJ264LmCKkBGDJvpX8o7GbWIrDE0o,15712
6
- annofabcli/annotation/change_annotation_attributes.py,sha256=Zjqax-sb7ujJnTUVv4io0GFmZzfqx0rpN2N1W86uVTs,17092
7
- annofabcli/annotation/change_annotation_attributes_per_annotation.py,sha256=iU199MlMfD-nfyRiHL44Zqrvr12vOeUHXyxCrDgaivI,14130
6
+ annofabcli/annotation/change_annotation_attributes.py,sha256=pCHT12ba5jsT9kqslUXqfy2QriRT8yiAzgHDuL1JkNE,17884
7
+ annofabcli/annotation/change_annotation_attributes_per_annotation.py,sha256=g_SqH1xzAdA8D1Hy1aoOPlpF86nc7F1PENeUX8XftDs,15529
8
8
  annofabcli/annotation/change_annotation_properties.py,sha256=Kp_LZ5sSoVmmjGE80ABVO3InxsXBIxiFFvVcIJNsOMk,18309
9
9
  annofabcli/annotation/copy_annotation.py,sha256=Pih2k3vvpgfT3Ovb3gZw2L_8fK_ws_wKR7ARYG5hG_8,18407
10
10
  annofabcli/annotation/delete_annotation.py,sha256=hQApNrx2Ci1bBWk0dRGA0oJkIgDHwl6Jy0-33gYF6jo,22989
11
11
  annofabcli/annotation/download_annotation_zip.py,sha256=P_ZpdqIaSFEmB8jjpdykcRhh2tVlHxSlXFrYreJjShE,3282
12
12
  annofabcli/annotation/dump_annotation.py,sha256=Q-p6f5XBs7khDgrfY5Q3CGLBMKEerJWO_CQ8_73UXVM,9972
13
- annofabcli/annotation/import_annotation.py,sha256=hVX2T7gN3pad2KI7TzB2_fnga7fnfRVTMGUa611m3xE,34612
13
+ annofabcli/annotation/import_annotation.py,sha256=w0iSTmkIY8tz3cTUy2FJ6LCVpVUtKzcD7ej2cznot4A,34533
14
14
  annofabcli/annotation/list_annotation.py,sha256=uKcOuGC7lzd6vVbzizkiZtYdXJ7EzY0iifuiqKl2wQM,10707
15
15
  annofabcli/annotation/list_annotation_count.py,sha256=T9fbaoxWeDJIVgW_YgHRldbwrVZWiE-57lfJrDQrj80,6474
16
16
  annofabcli/annotation/merge_segmentation.py,sha256=kIsCeXtJxzd6nobQPpi0fscaRDlTx3tg1qpy5PDfSJI,18107
@@ -182,7 +182,7 @@ annofabcli/supplementary/put_supplementary_data.py,sha256=Pyq9G6xQFyJ8qrdWLOQvIU
182
182
  annofabcli/supplementary/subcommand_supplementary.py,sha256=F8qfuNQzgW5HV1QKB4h0DWN7-kPVQcoFQwPfW_vjZVk,1079
183
183
  annofabcli/task/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
184
184
  annofabcli/task/cancel_acceptance.py,sha256=QG9GhGNfBXntqYTKdYjLUczjD4p_ATkPc_xYCpTO24Y,13851
185
- annofabcli/task/change_operator.py,sha256=1LLpez79ezW_MqcNN608kjxp3xTS1uRRioqcugui9hM,10446
185
+ annofabcli/task/change_operator.py,sha256=q6pMd1SdsTRgMHS0705dnosTSHprTpYgXtNd0rli2Zg,11793
186
186
  annofabcli/task/change_status_to_break.py,sha256=hwdFTFW-zV0VxuinoBB5n6mvHJ7g9ChjrSOXZcNk88w,8621
187
187
  annofabcli/task/change_status_to_on_hold.py,sha256=vWRyk6IK3HcgTWDIbbhXzsrtuoa7OlXCf8CvUpFp_Uw,12981
188
188
  annofabcli/task/complete_tasks.py,sha256=ssg_Z7ADRQRXvXgK2k5TEmvbRjrJQ33cXeb8kG8Y3jc,24917
@@ -209,8 +209,8 @@ annofabcli/task_history_event/download_task_history_event_json.py,sha256=hQLVbQ0
209
209
  annofabcli/task_history_event/list_all_task_history_event.py,sha256=EeKMyPUxGwYCFtWQHHW954ZserGm8lUqrwNnV1iX9X4,6830
210
210
  annofabcli/task_history_event/list_worktime.py,sha256=Y7Pu5DP7scPf7HPt6CTiTvB1_5_Nfi1bStUIaCpkhII,15507
211
211
  annofabcli/task_history_event/subcommand_task_history_event.py,sha256=mJVJoT4RXk4HWnY7-Nrsl4If-gtaIIEXd2z7eFZwM2I,1260
212
- annofabcli-1.106.6.dist-info/METADATA,sha256=6fqjbrE7DWDbXzS3Zo4GPm_oHeqhkdfjqqdUlySo8Rw,5286
213
- annofabcli-1.106.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
214
- annofabcli-1.106.6.dist-info/entry_points.txt,sha256=C2uSUc-kkLJpoK_mDL5FEMAdorLEMPfwSf8VBMYnIFM,56
215
- annofabcli-1.106.6.dist-info/licenses/LICENSE,sha256=pcqWYfxFtxBzhvKp3x9MXNM4xciGb2eFewaRhXUNHlo,1081
216
- annofabcli-1.106.6.dist-info/RECORD,,
212
+ annofabcli-1.106.8.dist-info/METADATA,sha256=rSWCLpho30p_wsQdKax1vujJoZ3Zlj03RAGDGcth2s8,5286
213
+ annofabcli-1.106.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
214
+ annofabcli-1.106.8.dist-info/entry_points.txt,sha256=C2uSUc-kkLJpoK_mDL5FEMAdorLEMPfwSf8VBMYnIFM,56
215
+ annofabcli-1.106.8.dist-info/licenses/LICENSE,sha256=pcqWYfxFtxBzhvKp3x9MXNM4xciGb2eFewaRhXUNHlo,1081
216
+ annofabcli-1.106.8.dist-info/RECORD,,