annofabcli 1.96.1__py3-none-any.whl → 1.98.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- annofabcli/__version__.py +1 -1
- annofabcli/annotation/merge_segmentation.py +390 -0
- annofabcli/annotation/remove_segmentation_overlap.py +343 -0
- annofabcli/annotation/subcommand_annotation.py +4 -0
- annofabcli/input_data/change_input_data_name.py +5 -7
- annofabcli/input_data/update_metadata_of_input_data.py +2 -1
- annofabcli/project_member/put_project_members.py +32 -44
- annofabcli/statistics/list_annotation_count.py +2 -2
- annofabcli/statistics/list_annotation_duration.py +2 -2
- annofabcli/statistics/visualization/dataframe/productivity_per_date.py +5 -5
- annofabcli/statistics/visualization/dataframe/task.py +26 -7
- annofabcli/statistics/visualization/dataframe/user_performance.py +3 -3
- annofabcli/statistics/visualization/dataframe/whole_performance.py +2 -2
- annofabcli/statistics/visualization/dataframe/whole_productivity_per_date.py +2 -2
- annofabcli/statistics/visualization/dataframe/worktime_per_date.py +1 -1
- annofabcli/supplementary/delete_supplementary_data.py +11 -18
- annofabcli/supplementary/put_supplementary_data.py +58 -81
- annofabcli/task/list_tasks_added_task_history.py +91 -8
- annofabcli/task/update_metadata_of_task.py +2 -1
- {annofabcli-1.96.1.dist-info → annofabcli-1.98.0.dist-info}/METADATA +3 -3
- {annofabcli-1.96.1.dist-info → annofabcli-1.98.0.dist-info}/RECORD +24 -22
- {annofabcli-1.96.1.dist-info → annofabcli-1.98.0.dist-info}/LICENSE +0 -0
- {annofabcli-1.96.1.dist-info → annofabcli-1.98.0.dist-info}/WHEEL +0 -0
- {annofabcli-1.96.1.dist-info → annofabcli-1.98.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import copy
|
|
5
|
+
import logging
|
|
6
|
+
import multiprocessing
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
from collections.abc import Collection
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Optional
|
|
12
|
+
|
|
13
|
+
import annofabapi
|
|
14
|
+
import numpy
|
|
15
|
+
from annofabapi.pydantic_models.task_status import TaskStatus
|
|
16
|
+
from annofabapi.segmentation import read_binary_image, write_binary_image
|
|
17
|
+
from annofabapi.utils import can_put_annotation
|
|
18
|
+
|
|
19
|
+
import annofabcli
|
|
20
|
+
from annofabcli.common.cli import (
|
|
21
|
+
COMMAND_LINE_ERROR_STATUS_CODE,
|
|
22
|
+
PARALLELISM_CHOICES,
|
|
23
|
+
ArgumentParser,
|
|
24
|
+
CommandLine,
|
|
25
|
+
CommandLineWithConfirm,
|
|
26
|
+
build_annofabapi_resource_and_login,
|
|
27
|
+
)
|
|
28
|
+
from annofabcli.common.facade import AnnofabApiFacade
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def remove_overlap_of_binary_image_array(
|
|
34
|
+
binary_image_array_by_annotation: dict[str, numpy.ndarray], annotation_id_list: list[str]
|
|
35
|
+
) -> dict[str, numpy.ndarray]:
|
|
36
|
+
"""
|
|
37
|
+
塗りつぶし画像の重なりを除去したbool配列をdictで返します。
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
binary_image_array_by_annotation: annotation_idをkeyとし、塗りつぶし画像のbool配列をvalueとするdict
|
|
41
|
+
annotation_id_list: 塗りつぶし画像のannotation_idのlist。背面から前面の順に格納されている
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
重なりを除去した塗りつぶし画像のbool配列が格納されているdict。keyはannotation_id
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
assert set(binary_image_array_by_annotation.keys()) == set(annotation_id_list)
|
|
48
|
+
|
|
49
|
+
whole_2d_array = None # 複数の塗りつぶしアノテーションを1枚に重ね合わせた状態。各要素はannotation_id
|
|
50
|
+
|
|
51
|
+
# 背面から塗りつぶしアノテーションのbool配列を重ねていく
|
|
52
|
+
for annotation_id in annotation_id_list:
|
|
53
|
+
input_binary_image_array = binary_image_array_by_annotation[annotation_id]
|
|
54
|
+
if whole_2d_array is None:
|
|
55
|
+
whole_2d_array = numpy.full(input_binary_image_array.shape, "", dtype=str)
|
|
56
|
+
|
|
57
|
+
whole_2d_array = numpy.where(input_binary_image_array, annotation_id, whole_2d_array)
|
|
58
|
+
|
|
59
|
+
output_binary_image_array_by_annotation = {}
|
|
60
|
+
for annotation_id in annotation_id_list:
|
|
61
|
+
output_binary_image_array: numpy.ndarray = whole_2d_array == annotation_id # type: ignore[assignment]
|
|
62
|
+
output_binary_image_array_by_annotation[annotation_id] = output_binary_image_array
|
|
63
|
+
|
|
64
|
+
return output_binary_image_array_by_annotation
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class RemoveSegmentationOverlapMain(CommandLineWithConfirm):
|
|
68
|
+
def __init__(self, annofab_service: annofabapi.Resource, *, project_id: str, all_yes: bool, is_force: bool) -> None:
|
|
69
|
+
self.annofab_service = annofab_service
|
|
70
|
+
self.project_id = project_id
|
|
71
|
+
self.is_force = is_force
|
|
72
|
+
super().__init__(all_yes)
|
|
73
|
+
|
|
74
|
+
def remove_segmentation_overlap_and_save(self, details: list[dict[str, Any]], output_dir: Path) -> list[str]:
|
|
75
|
+
"""
|
|
76
|
+
`getEditorAnnotation` APIで取得した`details`から、塗りつぶし画像の重なりの除去が必要な場合に、
|
|
77
|
+
重なりを除去した塗りつぶし画像を`output_dir`に出力します。
|
|
78
|
+
塗りつぶし画像のファイル名は`${annotation_id}.png`です。
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
details: `getEditorAnnotation` APIで取得した`details`
|
|
82
|
+
output_dir: 塗りつぶし画像の出力先のディレクトリ。
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
重なりの除去が必要な塗りつぶし画像のannotation_idのlist
|
|
86
|
+
"""
|
|
87
|
+
input_binary_image_array_by_annotation = {}
|
|
88
|
+
segmentation_annotation_id_list = []
|
|
89
|
+
|
|
90
|
+
for detail in details:
|
|
91
|
+
if detail["body"]["_type"] != "Outer":
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
segmentation_response = self.annofab_service.wrapper.execute_http_get(detail["body"]["url"], stream=True)
|
|
95
|
+
segmentation_response.raw.decode_content = True
|
|
96
|
+
input_binary_image_array_by_annotation[detail["annotation_id"]] = read_binary_image(segmentation_response.raw)
|
|
97
|
+
segmentation_annotation_id_list.append(detail["annotation_id"])
|
|
98
|
+
|
|
99
|
+
# reversedを使っている理由:
|
|
100
|
+
# `details`には、前面から背面の順にアノテーションが格納されているため、
|
|
101
|
+
output_binary_image_array_by_annotation = remove_overlap_of_binary_image_array(
|
|
102
|
+
input_binary_image_array_by_annotation, list(reversed(segmentation_annotation_id_list))
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
updated_annotation_id_list = []
|
|
106
|
+
for annotation_id, output_binary_image_array in output_binary_image_array_by_annotation.items():
|
|
107
|
+
input_binary_image_array = input_binary_image_array_by_annotation[annotation_id]
|
|
108
|
+
if not numpy.array_equal(input_binary_image_array, output_binary_image_array):
|
|
109
|
+
output_file_path = output_dir / f"{annotation_id}.png"
|
|
110
|
+
write_binary_image(output_binary_image_array, output_file_path)
|
|
111
|
+
updated_annotation_id_list.append(annotation_id)
|
|
112
|
+
|
|
113
|
+
return updated_annotation_id_list
|
|
114
|
+
|
|
115
|
+
def update_segmentation_annotation(self, task_id: str, input_data_id: str, log_message_prefix: str = "") -> bool:
|
|
116
|
+
"""
|
|
117
|
+
塗りつぶしアノテーションの重なりがあれば、`putAnnotation` APIを使用して重なりを除去します。
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
project_id: プロジェクトID
|
|
121
|
+
task_id: タスクID
|
|
122
|
+
annotation_id_list: 更新する塗りつぶし画像のannotation_idのlist
|
|
123
|
+
"""
|
|
124
|
+
old_annotation, _ = self.annofab_service.api.get_editor_annotation(self.project_id, task_id, input_data_id, query_params={"v": "2"})
|
|
125
|
+
old_details = old_annotation["details"]
|
|
126
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
127
|
+
temp_dir_path = Path(temp_dir)
|
|
128
|
+
updated_annotation_id_list = self.remove_segmentation_overlap_and_save(old_details, temp_dir_path)
|
|
129
|
+
if len(updated_annotation_id_list) == 0:
|
|
130
|
+
logger.debug(
|
|
131
|
+
f"{log_message_prefix}塗りつぶしアノテーションの重なりはなかったので、スキップします。 :: "
|
|
132
|
+
f"task_id='{task_id}', input_data_id='{input_data_id}'"
|
|
133
|
+
)
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
logger.debug(
|
|
137
|
+
f"{log_message_prefix}{len(updated_annotation_id_list)} 件の塗りつぶしアノテーションを更新します。 :: "
|
|
138
|
+
f"task_id='{task_id}', input_data_id='{input_data_id}', annotation_id_list={updated_annotation_id_list}"
|
|
139
|
+
)
|
|
140
|
+
new_details = []
|
|
141
|
+
for detail in old_details:
|
|
142
|
+
annotation_id = detail["annotation_id"]
|
|
143
|
+
new_detail = copy.deepcopy(detail)
|
|
144
|
+
new_detail["_type"] = "Update"
|
|
145
|
+
if annotation_id in updated_annotation_id_list:
|
|
146
|
+
with (temp_dir_path / f"{annotation_id}.png").open("rb") as f:
|
|
147
|
+
s3_path = self.annofab_service.wrapper.upload_data_to_s3(self.project_id, data=f, content_type="image/png")
|
|
148
|
+
|
|
149
|
+
new_detail["body"]["path"] = s3_path
|
|
150
|
+
|
|
151
|
+
else:
|
|
152
|
+
# 更新しない場合は、`body`をNoneにする
|
|
153
|
+
new_detail["body"] = None
|
|
154
|
+
|
|
155
|
+
new_details.append(new_detail)
|
|
156
|
+
|
|
157
|
+
request_body = {
|
|
158
|
+
"project_id": self.project_id,
|
|
159
|
+
"task_id": task_id,
|
|
160
|
+
"input_data_id": input_data_id,
|
|
161
|
+
"details": new_details,
|
|
162
|
+
"format_version": "2.0.0",
|
|
163
|
+
"updated_datetime": old_annotation["updated_datetime"],
|
|
164
|
+
}
|
|
165
|
+
self.annofab_service.api.put_annotation(self.project_id, task_id, input_data_id, query_params={"v": "2"}, request_body=request_body)
|
|
166
|
+
logger.debug(
|
|
167
|
+
f"{log_message_prefix}{len(updated_annotation_id_list)} 件の塗りつぶしアノテーションを更新しました。 :: "
|
|
168
|
+
f"task_id='{task_id}', input_data_id='{input_data_id}'"
|
|
169
|
+
)
|
|
170
|
+
return True
|
|
171
|
+
|
|
172
|
+
def update_segmentation_annotation_for_task(self, task_id: str, *, task_index: Optional[int] = None) -> int:
|
|
173
|
+
"""
|
|
174
|
+
1個のタスクに対して、塗りつぶしアノテーションの重なりを除去します。
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
アノテーションを更新した入力データ数(フレーム数)
|
|
178
|
+
"""
|
|
179
|
+
log_message_prefix = f"{task_index + 1} 件目 :: " if task_index is not None else ""
|
|
180
|
+
|
|
181
|
+
task = self.annofab_service.wrapper.get_task_or_none(project_id=self.project_id, task_id=task_id)
|
|
182
|
+
if task is None:
|
|
183
|
+
logger.warning(f"{log_message_prefix}task_id='{task_id}'であるタスクは存在しません。")
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
if task["status"] in {TaskStatus.WORKING.value, TaskStatus.COMPLETE.value}:
|
|
187
|
+
logger.debug(
|
|
188
|
+
f"{log_message_prefix}task_id='{task_id}'のタスクの状態は「作業中」または「完了」であるため、"
|
|
189
|
+
f"アノテーションの更新をスキップします。 :: status='{task['status']}'"
|
|
190
|
+
)
|
|
191
|
+
return 0
|
|
192
|
+
|
|
193
|
+
if not self.confirm_processing(f"task_id='{task_id}'の塗りつぶしアノテーションの重なりを除去しますか?"):
|
|
194
|
+
return 0
|
|
195
|
+
|
|
196
|
+
# 担当者割り当て変更チェック
|
|
197
|
+
changed_operator = False
|
|
198
|
+
original_operator_account_id = task["account_id"]
|
|
199
|
+
if not can_put_annotation(task, self.annofab_service.api.account_id):
|
|
200
|
+
if self.is_force:
|
|
201
|
+
logger.debug(f"{log_message_prefix}task_id='{task_id}' のタスクの担当者を自分自身に変更します。")
|
|
202
|
+
changed_operator = True
|
|
203
|
+
task = self.annofab_service.wrapper.change_task_operator(
|
|
204
|
+
self.project_id,
|
|
205
|
+
task_id,
|
|
206
|
+
self.annofab_service.api.account_id,
|
|
207
|
+
last_updated_datetime=task["updated_datetime"],
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
logger.debug(
|
|
211
|
+
f"{log_message_prefix}task_id='{task_id}' のタスクは、過去に誰かに割り当てられたタスクで、"
|
|
212
|
+
f"現在の担当者が自分自身でないため、アノテーションの更新をスキップします。"
|
|
213
|
+
f"担当者を自分自身に変更してアノテーションを更新する場合は、コマンドライン引数 '--force' を指定してください。"
|
|
214
|
+
)
|
|
215
|
+
return 0
|
|
216
|
+
|
|
217
|
+
success_input_data_count = 0
|
|
218
|
+
for input_data_id in task["input_data_id_list"]:
|
|
219
|
+
try:
|
|
220
|
+
result = self.update_segmentation_annotation(task_id, input_data_id, log_message_prefix=log_message_prefix)
|
|
221
|
+
if result:
|
|
222
|
+
success_input_data_count += 1
|
|
223
|
+
except Exception:
|
|
224
|
+
logger.warning(
|
|
225
|
+
f"{log_message_prefix}task_id='{task_id}', input_data_id='{input_data_id}'のアノテーションの更新に失敗しました。", exc_info=True
|
|
226
|
+
)
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
# 担当者を元に戻す
|
|
230
|
+
if changed_operator:
|
|
231
|
+
logger.debug(
|
|
232
|
+
f"{log_message_prefix}task_id='{task_id}' のタスクの担当者を、元の担当者(account_id='{original_operator_account_id}')に変更します。"
|
|
233
|
+
)
|
|
234
|
+
self.annofab_service.wrapper.change_task_operator(
|
|
235
|
+
self.project_id,
|
|
236
|
+
task_id,
|
|
237
|
+
original_operator_account_id,
|
|
238
|
+
last_updated_datetime=task["updated_datetime"],
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return success_input_data_count
|
|
242
|
+
|
|
243
|
+
def update_segmentation_annotation_for_task_wrapper(self, tpl: tuple[int, str]) -> int:
|
|
244
|
+
try:
|
|
245
|
+
task_index, task_id = tpl
|
|
246
|
+
return self.update_segmentation_annotation_for_task(task_id, task_index=task_index)
|
|
247
|
+
except Exception:
|
|
248
|
+
logger.warning(f"task_id='{task_id}' のアノテーションの更新に失敗しました。", exc_info=True)
|
|
249
|
+
return 0
|
|
250
|
+
|
|
251
|
+
def main(
|
|
252
|
+
self,
|
|
253
|
+
task_ids: Collection[str],
|
|
254
|
+
parallelism: Optional[int] = None,
|
|
255
|
+
) -> None:
|
|
256
|
+
logger.info(f"{len(task_ids)} 件のタスクの塗りつぶしアノテーションの重なりを除去します。")
|
|
257
|
+
success_input_data_count = 0
|
|
258
|
+
if parallelism is not None:
|
|
259
|
+
with multiprocessing.Pool(parallelism) as pool:
|
|
260
|
+
result_count_list = pool.map(self.update_segmentation_annotation_for_task_wrapper, enumerate(task_ids))
|
|
261
|
+
success_input_data_count = sum(result_count_list)
|
|
262
|
+
|
|
263
|
+
else:
|
|
264
|
+
for task_index, task_id in enumerate(task_ids):
|
|
265
|
+
try:
|
|
266
|
+
result = self.update_segmentation_annotation_for_task(task_id, task_index=task_index)
|
|
267
|
+
success_input_data_count += result
|
|
268
|
+
except Exception:
|
|
269
|
+
logger.warning(f"task_id='{task_id}' のアノテーションの更新に失敗しました。", exc_info=True)
|
|
270
|
+
continue
|
|
271
|
+
|
|
272
|
+
logger.info(f"{len(task_ids)} 件のタスクに含まれる入力データ {success_input_data_count} 件の塗りつぶしアノテーションを更新しました。")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class RemoveSegmentationOverlap(CommandLine):
|
|
276
|
+
COMMON_MESSAGE = "annofabcli annotation remove_segmentation_overlap: error:"
|
|
277
|
+
|
|
278
|
+
def validate(self, args: argparse.Namespace) -> bool:
|
|
279
|
+
if args.parallelism is not None and not args.yes:
|
|
280
|
+
print( # noqa: T201
|
|
281
|
+
f"{self.COMMON_MESSAGE} argument --parallelism: '--parallelism'を指定するときは、'--yes' を指定してください。",
|
|
282
|
+
file=sys.stderr,
|
|
283
|
+
)
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
return True
|
|
287
|
+
|
|
288
|
+
def main(self) -> None:
|
|
289
|
+
args = self.args
|
|
290
|
+
if not self.validate(args):
|
|
291
|
+
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
292
|
+
|
|
293
|
+
project_id = args.project_id
|
|
294
|
+
task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id)
|
|
295
|
+
|
|
296
|
+
main_obj = RemoveSegmentationOverlapMain(
|
|
297
|
+
self.service,
|
|
298
|
+
project_id=project_id,
|
|
299
|
+
all_yes=self.all_yes,
|
|
300
|
+
is_force=args.force,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
main_obj.main(task_id_list, parallelism=args.parallelism)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def main(args: argparse.Namespace) -> None:
|
|
307
|
+
service = build_annofabapi_resource_and_login(args)
|
|
308
|
+
facade = AnnofabApiFacade(service)
|
|
309
|
+
RemoveSegmentationOverlap(service, facade, args).main()
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
313
|
+
argument_parser = ArgumentParser(parser)
|
|
314
|
+
argument_parser.add_project_id()
|
|
315
|
+
argument_parser.add_task_id()
|
|
316
|
+
|
|
317
|
+
parser.add_argument(
|
|
318
|
+
"--force",
|
|
319
|
+
action="store_true",
|
|
320
|
+
help="過去に担当者を割り当てられていて、かつ現在の担当者が自分自身でない場合、タスクの担当者を一時的に自分自身に変更してからアノテーションをコピーします。",
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
parser.add_argument(
|
|
324
|
+
"--parallelism",
|
|
325
|
+
type=int,
|
|
326
|
+
choices=PARALLELISM_CHOICES,
|
|
327
|
+
help="並列度。指定しない場合は、逐次的に処理します。指定した場合は、``--yes`` も指定してください。",
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
parser.set_defaults(subcommand_func=main)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
|
|
334
|
+
subcommand_name = "remove_segmentation_overlap"
|
|
335
|
+
subcommand_help = "塗りつぶしアノテーションの重なりを除去します。"
|
|
336
|
+
description = (
|
|
337
|
+
"塗りつぶしアノテーションの重なりを除去します。"
|
|
338
|
+
"Annofabでインスタンスセグメンテーションは重ねることができてしまいます。"
|
|
339
|
+
"この重なりをなくしたいときに有用です。"
|
|
340
|
+
)
|
|
341
|
+
parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description)
|
|
342
|
+
parse_args(parser)
|
|
343
|
+
return parser
|
|
@@ -10,6 +10,8 @@ import annofabcli.annotation.dump_annotation
|
|
|
10
10
|
import annofabcli.annotation.import_annotation
|
|
11
11
|
import annofabcli.annotation.list_annotation
|
|
12
12
|
import annofabcli.annotation.list_annotation_count
|
|
13
|
+
import annofabcli.annotation.merge_segmentation
|
|
14
|
+
import annofabcli.annotation.remove_segmentation_overlap
|
|
13
15
|
import annofabcli.annotation.restore_annotation
|
|
14
16
|
import annofabcli.common.cli
|
|
15
17
|
|
|
@@ -27,6 +29,8 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
27
29
|
annofabcli.annotation.import_annotation.add_parser(subparsers)
|
|
28
30
|
annofabcli.annotation.list_annotation.add_parser(subparsers)
|
|
29
31
|
annofabcli.annotation.list_annotation_count.add_parser(subparsers)
|
|
32
|
+
annofabcli.annotation.merge_segmentation.add_parser(subparsers)
|
|
33
|
+
annofabcli.annotation.remove_segmentation_overlap.add_parser(subparsers)
|
|
30
34
|
annofabcli.annotation.restore_annotation.add_parser(subparsers)
|
|
31
35
|
|
|
32
36
|
|
|
@@ -143,11 +143,9 @@ def create_changed_input_data_list_from_csv(csv_file: Path) -> list[ChangedInput
|
|
|
143
143
|
変更対象の入力データのlist
|
|
144
144
|
"""
|
|
145
145
|
df_input_data = pandas.read_csv(
|
|
146
|
-
|
|
147
|
-
header=None,
|
|
148
|
-
names=("input_data_id", "input_data_name"),
|
|
146
|
+
csv_file,
|
|
149
147
|
# 文字列として読み込むようにする
|
|
150
|
-
dtype={"input_data_id":
|
|
148
|
+
dtype={"input_data_id": "string", "input_data_name": "string"},
|
|
151
149
|
)
|
|
152
150
|
|
|
153
151
|
input_data_dict_list = df_input_data.to_dict("records")
|
|
@@ -211,9 +209,9 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
211
209
|
"変更対象の入力データが記載されたCSVファイルのパスを指定してください。\n"
|
|
212
210
|
"CSVのフォーマットは以下の通りです。"
|
|
213
211
|
"\n"
|
|
214
|
-
" *
|
|
215
|
-
" *
|
|
216
|
-
" *
|
|
212
|
+
" * ヘッダ行あり, カンマ区切り\n"
|
|
213
|
+
" * input_data_id (required)\n"
|
|
214
|
+
" * input_data_name (required)\n"
|
|
217
215
|
),
|
|
218
216
|
)
|
|
219
217
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import copy
|
|
4
5
|
import json
|
|
5
6
|
import logging
|
|
6
7
|
import multiprocessing
|
|
@@ -182,7 +183,7 @@ class UpdateMetadata(CommandLine):
|
|
|
182
183
|
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
183
184
|
|
|
184
185
|
assert input_data_id_list is not None, "'--metadata'を指定したときは'--input_data_id'は必須です。"
|
|
185
|
-
metadata_by_input_data_id = {input_data_id: metadata for input_data_id in input_data_id_list}
|
|
186
|
+
metadata_by_input_data_id = {input_data_id: copy.deepcopy(metadata) for input_data_id in input_data_id_list}
|
|
186
187
|
|
|
187
188
|
elif args.metadata_by_input_data_id is not None:
|
|
188
189
|
metadata_by_input_data_id = annofabcli.common.cli.get_json_from_args(args.metadata_by_input_data_id)
|
|
@@ -5,7 +5,6 @@ from pathlib import Path
|
|
|
5
5
|
from typing import Any, Optional
|
|
6
6
|
|
|
7
7
|
import more_itertools
|
|
8
|
-
import numpy
|
|
9
8
|
import pandas
|
|
10
9
|
import requests
|
|
11
10
|
from annofabapi.models import ProjectMemberRole, ProjectMemberStatus
|
|
@@ -26,8 +25,8 @@ class Member(DataClassJsonMixin):
|
|
|
26
25
|
|
|
27
26
|
user_id: str
|
|
28
27
|
member_role: ProjectMemberRole
|
|
29
|
-
sampling_inspection_rate: Optional[int]
|
|
30
|
-
sampling_acceptance_rate: Optional[int]
|
|
28
|
+
sampling_inspection_rate: Optional[int] = None
|
|
29
|
+
sampling_acceptance_rate: Optional[int] = None
|
|
31
30
|
|
|
32
31
|
|
|
33
32
|
class PutProjectMembers(CommandLine):
|
|
@@ -44,7 +43,7 @@ class PutProjectMembers(CommandLine):
|
|
|
44
43
|
def member_exists(members: list[dict[str, Any]], user_id: str) -> bool:
|
|
45
44
|
return PutProjectMembers.find_member(members, user_id) is not None
|
|
46
45
|
|
|
47
|
-
def invite_project_member(self, project_id: str, member: Member, old_project_members: list[dict[str, Any]])
|
|
46
|
+
def invite_project_member(self, project_id: str, member: Member, old_project_members: list[dict[str, Any]]) -> dict[str, Any]:
|
|
48
47
|
old_member = self.find_member(old_project_members, member.user_id)
|
|
49
48
|
last_updated_datetime = old_member["updated_datetime"] if old_member is not None else None
|
|
50
49
|
|
|
@@ -58,7 +57,7 @@ class PutProjectMembers(CommandLine):
|
|
|
58
57
|
updated_project_member = self.service.api.put_project_member(project_id, member.user_id, request_body=request_body)[0]
|
|
59
58
|
return updated_project_member
|
|
60
59
|
|
|
61
|
-
def delete_project_member(self, project_id: str, deleted_member: dict[str, Any])
|
|
60
|
+
def delete_project_member(self, project_id: str, deleted_member: dict[str, Any]) -> dict[str, Any]:
|
|
62
61
|
request_body = {
|
|
63
62
|
"member_status": ProjectMemberStatus.INACTIVE.value,
|
|
64
63
|
"member_role": deleted_member["member_role"],
|
|
@@ -67,7 +66,7 @@ class PutProjectMembers(CommandLine):
|
|
|
67
66
|
updated_project_member = self.service.api.put_project_member(project_id, deleted_member["user_id"], request_body=request_body)[0]
|
|
68
67
|
return updated_project_member
|
|
69
68
|
|
|
70
|
-
def put_project_members(self, project_id: str, members: list[Member], delete: bool = False)
|
|
69
|
+
def put_project_members(self, project_id: str, members: list[Member], *, delete: bool = False) -> None:
|
|
71
70
|
"""
|
|
72
71
|
プロジェクトメンバを一括で登録する。
|
|
73
72
|
|
|
@@ -88,7 +87,7 @@ class PutProjectMembers(CommandLine):
|
|
|
88
87
|
|
|
89
88
|
count_invite_members = 0
|
|
90
89
|
# プロジェクトメンバを登録
|
|
91
|
-
logger.info(f"{project_title} に、{len(members)} 件のプロジェクトメンバを登録します。")
|
|
90
|
+
logger.info(f"プロジェクト '{project_title}' に、{len(members)} 件のプロジェクトメンバを登録します。")
|
|
92
91
|
for member in members:
|
|
93
92
|
if member.user_id == self.service.api.login_user_id:
|
|
94
93
|
logger.debug(f"ユーザ '{member.user_id}'は自分自身なので、登録しません。")
|
|
@@ -99,7 +98,7 @@ class PutProjectMembers(CommandLine):
|
|
|
99
98
|
continue
|
|
100
99
|
|
|
101
100
|
message_for_confirm = (
|
|
102
|
-
f"ユーザ '{member.user_id}'
|
|
101
|
+
f"ユーザ '{member.user_id}'を、プロジェクト'{project_title}'のメンバーに登録しますか? member_role='{member.member_role.value}'"
|
|
103
102
|
)
|
|
104
103
|
if not self.confirm_processing(message_for_confirm):
|
|
105
104
|
continue
|
|
@@ -107,14 +106,15 @@ class PutProjectMembers(CommandLine):
|
|
|
107
106
|
# メンバを登録
|
|
108
107
|
try:
|
|
109
108
|
self.invite_project_member(project_id, member, old_project_members)
|
|
110
|
-
logger.debug(f"user_id = {member.user_id}, member_role = {member.member_role.value} のユーザをプロジェクトメンバに登録しました。")
|
|
109
|
+
logger.debug(f"user_id = '{member.user_id}', member_role = '{member.member_role.value}' のユーザをプロジェクトメンバに登録しました。")
|
|
111
110
|
count_invite_members += 1
|
|
112
111
|
|
|
113
|
-
except requests.exceptions.HTTPError
|
|
114
|
-
logger.warning(
|
|
115
|
-
|
|
112
|
+
except requests.exceptions.HTTPError:
|
|
113
|
+
logger.warning(
|
|
114
|
+
f"プロジェクトメンバの登録に失敗しました。user_id = '{member.user_id}', member_role = '{member.member_role.value}'", exc_info=True
|
|
115
|
+
)
|
|
116
116
|
|
|
117
|
-
logger.info(f"{project_title} に、{count_invite_members} / {len(members)} 件のプロジェクトメンバを登録しました。")
|
|
117
|
+
logger.info(f"プロジェクト'{project_title}' に、{count_invite_members} / {len(members)} 件のプロジェクトメンバを登録しました。")
|
|
118
118
|
|
|
119
119
|
# プロジェクトメンバを削除
|
|
120
120
|
if delete:
|
|
@@ -125,7 +125,7 @@ class PutProjectMembers(CommandLine):
|
|
|
125
125
|
]
|
|
126
126
|
|
|
127
127
|
count_delete_members = 0
|
|
128
|
-
logger.info(f"{project_title} から、{len(deleted_members)} 件のプロジェクトメンバを削除します。")
|
|
128
|
+
logger.info(f"プロジェクト '{project_title}' から、{len(deleted_members)} 件のプロジェクトメンバを削除します。")
|
|
129
129
|
for deleted_member in deleted_members:
|
|
130
130
|
message_for_confirm = f"ユーザ '{deleted_member['user_id']}'を、{project_title} のプロジェクトメンバから削除しますか?"
|
|
131
131
|
if not self.confirm_processing(message_for_confirm):
|
|
@@ -135,31 +135,18 @@ class PutProjectMembers(CommandLine):
|
|
|
135
135
|
self.delete_project_member(project_id, deleted_member)
|
|
136
136
|
logger.debug(f"ユーザ '{deleted_member['user_id']}' をプロジェクトメンバから削除しました。")
|
|
137
137
|
count_delete_members += 1
|
|
138
|
-
except requests.exceptions.HTTPError
|
|
139
|
-
logger.warning(
|
|
140
|
-
logger.warning(f"プロジェクトメンバの削除に失敗しました。user_id = '{deleted_member['user_id']}' ")
|
|
138
|
+
except requests.exceptions.HTTPError:
|
|
139
|
+
logger.warning(f"プロジェクトメンバの削除に失敗しました。user_id = '{deleted_member['user_id']}' ", exc_info=True)
|
|
141
140
|
|
|
142
|
-
logger.info(f"{project_title} から {count_delete_members} / {len(deleted_members)} 件のプロジェクトメンバを削除しました。")
|
|
141
|
+
logger.info(f"プロジェクト '{project_title}' から {count_delete_members} / {len(deleted_members)} 件のプロジェクトメンバを削除しました。")
|
|
143
142
|
|
|
144
143
|
@staticmethod
|
|
145
144
|
def get_members_from_csv(csv_path: Path) -> list[Member]:
|
|
146
|
-
def create_member(e): # noqa: ANN001, ANN202
|
|
147
|
-
return Member(
|
|
148
|
-
user_id=e.user_id,
|
|
149
|
-
member_role=ProjectMemberRole(e.member_role),
|
|
150
|
-
sampling_inspection_rate=e.sampling_inspection_rate,
|
|
151
|
-
sampling_acceptance_rate=e.sampling_acceptance_rate,
|
|
152
|
-
)
|
|
153
|
-
|
|
154
145
|
df = pandas.read_csv(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
# IDは必ず文字列として読み込むようにする
|
|
160
|
-
dtype={"user_id": str},
|
|
161
|
-
).replace({numpy.nan: None})
|
|
162
|
-
members = [create_member(e) for e in df.itertuples()]
|
|
146
|
+
csv_path,
|
|
147
|
+
dtype={"user_id": "string", "member_role": "string", "sampling_inspection_rate": "Int64", "sampling_acceptance_rate": "Int64"},
|
|
148
|
+
)
|
|
149
|
+
members = [Member.from_dict(e) for e in df.to_dict("records")]
|
|
163
150
|
return members
|
|
164
151
|
|
|
165
152
|
def main(self) -> None:
|
|
@@ -184,12 +171,14 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
184
171
|
type=str,
|
|
185
172
|
required=True,
|
|
186
173
|
help=(
|
|
187
|
-
"プロジェクトメンバが記載された
|
|
188
|
-
"CSV
|
|
189
|
-
"
|
|
190
|
-
"
|
|
191
|
-
"
|
|
192
|
-
"
|
|
174
|
+
"プロジェクトメンバが記載されたCSVファイルのパスを指定してください。"
|
|
175
|
+
"CSVのフォーマットは、ヘッダあり、カンマ区切りです。\n"
|
|
176
|
+
" * user_id (required)\n"
|
|
177
|
+
" * member_role (required)\n"
|
|
178
|
+
" * sampling_inspection_rate\n"
|
|
179
|
+
" * sampling_acceptance_rate\n"
|
|
180
|
+
"member_roleには ``owner``, ``worker``, ``accepter``, ``training_data_user`` のいずれかを指定します。\n"
|
|
181
|
+
"自分自身は登録できません。"
|
|
193
182
|
),
|
|
194
183
|
)
|
|
195
184
|
|
|
@@ -202,10 +191,9 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
202
191
|
|
|
203
192
|
def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
|
|
204
193
|
subcommand_name = "put"
|
|
205
|
-
subcommand_help = "
|
|
206
|
-
|
|
207
|
-
epilog = "オーナロールを持つユーザで実行してください。"
|
|
194
|
+
subcommand_help = "プロジェクトメンバを登録します。"
|
|
195
|
+
epilog = "オーナーロールを持つユーザで実行してください。"
|
|
208
196
|
|
|
209
|
-
parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help,
|
|
197
|
+
parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, epilog=epilog)
|
|
210
198
|
parse_args(parser)
|
|
211
199
|
return parser
|
|
@@ -552,7 +552,7 @@ class AttributeCountCsv:
|
|
|
552
552
|
|
|
553
553
|
# アノテーション数の列のNaNを0に変換する
|
|
554
554
|
value_columns = self._value_columns(counter_list, prior_attribute_columns)
|
|
555
|
-
df = df.fillna(
|
|
555
|
+
df = df.fillna(dict.fromkeys(value_columns, 0))
|
|
556
556
|
|
|
557
557
|
print_csv(df, output=str(output_file), to_csv_kwargs=self.csv_format)
|
|
558
558
|
|
|
@@ -655,7 +655,7 @@ class LabelCountCsv:
|
|
|
655
655
|
|
|
656
656
|
# アノテーション数列のNaNを0に変換する
|
|
657
657
|
value_columns = self._value_columns(counter_list, prior_label_columns)
|
|
658
|
-
df = df.fillna(
|
|
658
|
+
df = df.fillna(dict.fromkeys(value_columns, 0))
|
|
659
659
|
|
|
660
660
|
print_csv(df, output=str(output_file), to_csv_kwargs=self.csv_format)
|
|
661
661
|
|
|
@@ -411,7 +411,7 @@ class AnnotationDurationCsvByAttribute:
|
|
|
411
411
|
|
|
412
412
|
# アノテーション数の列のNaNを0に変換する
|
|
413
413
|
value_columns = self._value_columns(annotation_duration_list, prior_attribute_columns)
|
|
414
|
-
df = df.fillna(
|
|
414
|
+
df = df.fillna(dict.fromkeys(value_columns, 0))
|
|
415
415
|
return df
|
|
416
416
|
|
|
417
417
|
|
|
@@ -473,7 +473,7 @@ class AnnotationDurationCsvByLabel:
|
|
|
473
473
|
|
|
474
474
|
# アノテーション数列のNaNを0に変換する
|
|
475
475
|
value_columns = self._value_columns(annotation_duration_list, prior_label_columns)
|
|
476
|
-
df = df.fillna(
|
|
476
|
+
df = df.fillna(dict.fromkeys(value_columns, 0))
|
|
477
477
|
|
|
478
478
|
return df
|
|
479
479
|
|
|
@@ -181,17 +181,17 @@ class AbstractPhaseProductivityPerDate(abc.ABC):
|
|
|
181
181
|
|
|
182
182
|
# その他の欠損値(作業時間や生産量)を0で埋める
|
|
183
183
|
df2 = df2.fillna(
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
for col in [
|
|
184
|
+
dict.fromkeys(
|
|
185
|
+
[
|
|
187
186
|
"annotation_worktime_hour",
|
|
188
187
|
"inspection_worktime_hour",
|
|
189
188
|
"acceptance_worktime_hour",
|
|
190
189
|
"task_count",
|
|
191
190
|
"inspection_comment_count",
|
|
192
191
|
*self.production_volume_columns,
|
|
193
|
-
]
|
|
194
|
-
|
|
192
|
+
],
|
|
193
|
+
0,
|
|
194
|
+
)
|
|
195
195
|
)
|
|
196
196
|
|
|
197
197
|
return df2
|