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.
- annofabcli/annotation/change_annotation_attributes.py +35 -30
- annofabcli/annotation/change_annotation_attributes_per_annotation.py +49 -25
- annofabcli/annotation/import_annotation.py +25 -25
- annofabcli/task/change_operator.py +35 -17
- {annofabcli-1.106.6.dist-info → annofabcli-1.106.8.dist-info}/METADATA +1 -1
- {annofabcli-1.106.6.dist-info → annofabcli-1.106.8.dist-info}/RECORD +9 -9
- {annofabcli-1.106.6.dist-info → annofabcli-1.106.8.dist-info}/WHEEL +0 -0
- {annofabcli-1.106.6.dist-info → annofabcli-1.106.8.dist-info}/entry_points.txt +0 -0
- {annofabcli-1.106.6.dist-info → annofabcli-1.106.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -46,7 +46,7 @@ class ChangeAnnotationAttributesMain(CommandLineWithConfirm):
|
|
|
46
46
|
service: annofabapi.Resource,
|
|
47
47
|
*,
|
|
48
48
|
project_id: str,
|
|
49
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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}'
|
|
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
|
-
|
|
223
|
-
success_count = len([e for e in
|
|
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}'
|
|
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.
|
|
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 --
|
|
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,
|
|
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
|
-
"--
|
|
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
|
-
"
|
|
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
|
|
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]) ->
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
"
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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,
|
|
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="
|
|
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:
|
|
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
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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"
|
|
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"
|
|
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"
|
|
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)
|
|
@@ -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=
|
|
7
|
-
annofabcli/annotation/change_annotation_attributes_per_annotation.py,sha256=
|
|
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=
|
|
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=
|
|
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.
|
|
213
|
-
annofabcli-1.106.
|
|
214
|
-
annofabcli-1.106.
|
|
215
|
-
annofabcli-1.106.
|
|
216
|
-
annofabcli-1.106.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|