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.
Files changed (24) hide show
  1. annofabcli/__version__.py +1 -1
  2. annofabcli/annotation/merge_segmentation.py +390 -0
  3. annofabcli/annotation/remove_segmentation_overlap.py +343 -0
  4. annofabcli/annotation/subcommand_annotation.py +4 -0
  5. annofabcli/input_data/change_input_data_name.py +5 -7
  6. annofabcli/input_data/update_metadata_of_input_data.py +2 -1
  7. annofabcli/project_member/put_project_members.py +32 -44
  8. annofabcli/statistics/list_annotation_count.py +2 -2
  9. annofabcli/statistics/list_annotation_duration.py +2 -2
  10. annofabcli/statistics/visualization/dataframe/productivity_per_date.py +5 -5
  11. annofabcli/statistics/visualization/dataframe/task.py +26 -7
  12. annofabcli/statistics/visualization/dataframe/user_performance.py +3 -3
  13. annofabcli/statistics/visualization/dataframe/whole_performance.py +2 -2
  14. annofabcli/statistics/visualization/dataframe/whole_productivity_per_date.py +2 -2
  15. annofabcli/statistics/visualization/dataframe/worktime_per_date.py +1 -1
  16. annofabcli/supplementary/delete_supplementary_data.py +11 -18
  17. annofabcli/supplementary/put_supplementary_data.py +58 -81
  18. annofabcli/task/list_tasks_added_task_history.py +91 -8
  19. annofabcli/task/update_metadata_of_task.py +2 -1
  20. {annofabcli-1.96.1.dist-info → annofabcli-1.98.0.dist-info}/METADATA +3 -3
  21. {annofabcli-1.96.1.dist-info → annofabcli-1.98.0.dist-info}/RECORD +24 -22
  22. {annofabcli-1.96.1.dist-info → annofabcli-1.98.0.dist-info}/LICENSE +0 -0
  23. {annofabcli-1.96.1.dist-info → annofabcli-1.98.0.dist-info}/WHEEL +0 -0
  24. {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
- str(csv_file),
147
- header=None,
148
- names=("input_data_id", "input_data_name"),
146
+ csv_file,
149
147
  # 文字列として読み込むようにする
150
- dtype={"input_data_id": str, "input_data_name": str},
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
- " * ヘッダ行なし, カンマ区切り\n"
215
- " * 1列目: input_data_id (required)\n"
216
- " * 2列目: input_data_name (required)\n"
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]]): # noqa: ANN201
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]): # noqa: ANN201
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): # noqa: ANN201, FBT001, FBT002
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}'を、{project_title} プロジェクトのメンバに登録しますか?member_role={member.member_role.value}"
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 as e:
114
- logger.warning(e)
115
- logger.warning(f"プロジェクトメンバの登録に失敗しました。user_id = {member.user_id}, member_role = {member.member_role.value}")
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 as e:
139
- logger.warning(e)
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
- str(csv_path),
156
- sep=",",
157
- header=None,
158
- names=("user_id", "member_role", "sampling_inspection_rate", "sampling_acceptance_rate"),
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
- "プロジェクトメンバが記載されたCVファイルのパスを指定してください。"
188
- "CSVのフォーマットは、「1列目:user_id(required), 2列目:member_role(required), "
189
- "3列目:sampling_inspection_rate, 4列目:sampling_acceptance_rate, ヘッダ行なし, カンマ区切り」です。"
190
- "member_roleは ``owner``, ``worker``, ``accepter``, ``training_data_user`` のいずれかです。"
191
- "sampling_inspection_rate, sampling_acceptance_rate を省略した場合は未設定になります。"
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
- description = "プロジェクトメンバを登録する。"
207
- epilog = "オーナロールを持つユーザで実行してください。"
194
+ subcommand_help = "プロジェクトメンバを登録します。"
195
+ epilog = "オーナーロールを持つユーザで実行してください。"
208
196
 
209
- parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog)
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({column: 0 for column in value_columns})
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({column: 0 for column in value_columns})
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({column: 0 for column in value_columns})
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({column: 0 for column in value_columns})
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
- col: 0
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