annofabcli 1.114.5__py3-none-any.whl → 1.114.7__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 (29) hide show
  1. annofabcli/annotation/change_annotation_attributes.py +8 -8
  2. annofabcli/annotation/create_classification_annotation.py +26 -9
  3. annofabcli/annotation/delete_annotation.py +14 -14
  4. annofabcli/annotation/import_annotation.py +57 -19
  5. annofabcli/annotation_zip/list_annotation_3d_bounding_box.py +2 -2
  6. annofabcli/annotation_zip/list_annotation_bounding_box_2d.py +2 -2
  7. annofabcli/annotation_zip/list_polygon_annotation.py +2 -2
  8. annofabcli/annotation_zip/list_polyline_annotation.py +2 -2
  9. annofabcli/annotation_zip/list_range_annotation.py +2 -2
  10. annofabcli/annotation_zip/list_single_point_annotation.py +2 -2
  11. annofabcli/comment/delete_comment.py +2 -2
  12. annofabcli/comment/put_comment.py +179 -36
  13. annofabcli/comment/put_comment_simply.py +7 -4
  14. annofabcli/comment/put_inspection_comment.py +5 -1
  15. annofabcli/comment/put_onhold_comment.py +5 -1
  16. annofabcli/filesystem/draw_annotation.py +1 -1
  17. annofabcli/input_data/list_all_input_data_merged_task.py +3 -3
  18. annofabcli/instruction/copy_instruction.py +1 -1
  19. annofabcli/statistics/histogram.py +1 -1
  20. annofabcli/statistics/list_annotation_count.py +4 -4
  21. annofabcli/statistics/list_annotation_duration.py +1 -1
  22. annofabcli/statistics/list_video_duration.py +1 -1
  23. annofabcli/task/change_operator.py +3 -3
  24. annofabcli/task/complete_tasks.py +5 -3
  25. {annofabcli-1.114.5.dist-info → annofabcli-1.114.7.dist-info}/METADATA +1 -1
  26. {annofabcli-1.114.5.dist-info → annofabcli-1.114.7.dist-info}/RECORD +29 -29
  27. {annofabcli-1.114.5.dist-info → annofabcli-1.114.7.dist-info}/WHEEL +0 -0
  28. {annofabcli-1.114.5.dist-info → annofabcli-1.114.7.dist-info}/entry_points.txt +0 -0
  29. {annofabcli-1.114.5.dist-info → annofabcli-1.114.7.dist-info}/licenses/LICENSE +0 -0
@@ -9,6 +9,7 @@ from typing import Any
9
9
  import annofabapi
10
10
  import requests
11
11
  from annofabapi.models import CommentType, TaskPhase, TaskStatus
12
+ from annofabapi.pydantic_models.input_data_type import InputDataType
12
13
  from dataclasses_json import DataClassJsonMixin
13
14
 
14
15
  from annofabcli.comment.utils import get_comment_type_name
@@ -18,6 +19,18 @@ from annofabcli.common.type_util import assert_noreturn
18
19
 
19
20
  logger = logging.getLogger(__name__)
20
21
 
22
+ SUPPORTED_ANNOTATION_TYPES_FOR_INSPECTION_DATA = frozenset(
23
+ [
24
+ "user_bounding_box",
25
+ "bounding_box",
26
+ "polygon",
27
+ "polyline",
28
+ "point",
29
+ "range",
30
+ ]
31
+ )
32
+ """検査コメントの座標情報(InspectionData形式)への変換をサポートしているアノテーションタイプ"""
33
+
21
34
 
22
35
  @dataclass
23
36
  class AddedComment(DataClassJsonMixin):
@@ -28,15 +41,18 @@ class AddedComment(DataClassJsonMixin):
28
41
  comment: str
29
42
  """コメントの中身"""
30
43
 
31
- data: dict[str, Any] | None
44
+ data: dict[str, Any] | None = None
32
45
  """コメントを付与する位置や区間"""
33
46
 
34
- annotation_id: str | None
47
+ annotation_id: str | None = None
35
48
  """コメントに紐付けるアノテーションID"""
36
49
 
37
- phrases: list[str] | None
50
+ phrases: list[str] | None = None
38
51
  """参照している定型指摘ID"""
39
52
 
53
+ comment_id: str | None = None
54
+ """コメントID。省略時はUUIDv4が自動生成される。"""
55
+
40
56
 
41
57
  AddedCommentsForTask = dict[str, list[AddedComment]]
42
58
  """
@@ -51,6 +67,71 @@ keyはtask_id
51
67
  """
52
68
 
53
69
 
70
+ def convert_annotation_body_to_inspection_data( # noqa: PLR0911
71
+ annotation_body: dict[str, Any],
72
+ annotation_type: str,
73
+ input_data_type: InputDataType,
74
+ ) -> dict[str, Any]:
75
+ """
76
+ アノテーションのbody部分を、検査コメントの座標情報(`InspectionData`形式)に変換する。
77
+
78
+ Args:
79
+ annotation_body: Annotationの座標情報。AnnotationDetailContentOutputのdict形式
80
+ annotation_type: アノテーションタイプ。`SUPPORTED_ANNOTATION_TYPES_FOR_INSPECTION_DATA`のいずれかを指定すること。
81
+ input_data_type: プロジェクトの入力データタイプ。
82
+
83
+ Returns:
84
+ 検査コメントの座標情報(`InspectionData`形式)
85
+ """
86
+ match input_data_type:
87
+ case InputDataType.MOVIE:
88
+ match annotation_type:
89
+ case "range":
90
+ # 区間の開始位置をTime形式に変換
91
+ return {"start": annotation_body["data"]["begin"], "end": annotation_body["data"]["end"], "_type": "Time"}
92
+ case _:
93
+ # "Classification"など、Time形式以外のアノテーションタイプ
94
+ return {"start": 0, "end": 100, "_type": "Time"}
95
+
96
+ case InputDataType.CUSTOM:
97
+ match annotation_type:
98
+ case "user_bounding_box":
99
+ # 3次元バウンディングボックスの場合、data.dataの文字列をパースして返す
100
+ return {"data": annotation_body["data"]["data"], "_type": "Custom"}
101
+ case _:
102
+ # セグメントなど
103
+ return {
104
+ "data": '{"kind": "CUBOID", "shape": {"dimensions": {"width": 1.0, "height": 1.0, "depth": 1.0}, "location": {"x": 0.0, "y": 0.0, "z": 0.0}, "rotation": {"x": 0.0, "y": 0.0, "z": 0.0}, "direction": {"front": {"x": 1.0, "y": 0.0, "z": 0.0}, "up": {"x": 0.0, "y": 0.0, "z": 1.0}}}, "version": "2"}', # noqa: E501
105
+ "_type": "Custom",
106
+ }
107
+
108
+ case InputDataType.IMAGE:
109
+ match annotation_type:
110
+ case "bounding_box":
111
+ # 中心点を計算
112
+ left_top = annotation_body["data"]["left_top"]
113
+ right_bottom = annotation_body["data"]["right_bottom"]
114
+ center_x = (left_top["x"] + right_bottom["x"]) / 2
115
+ center_y = (left_top["y"] + right_bottom["y"]) / 2
116
+ return {"x": center_x, "y": center_y, "_type": "Point"}
117
+ case "polygon" | "polyline":
118
+ # 先頭の点を取得
119
+ points = annotation_body["data"]["points"]
120
+ assert len(points) > 0
121
+ first_point = points[0]
122
+ return {"x": first_point["x"], "y": first_point["y"], "_type": "Point"}
123
+ case "point":
124
+ # 点の形式に変換(pointキーから取り出す)
125
+ point = annotation_body["data"]["point"]
126
+ return {"x": point["x"], "y": point["y"], "_type": "Point"}
127
+ case "range":
128
+ # 区間の開始位置をTime形式に変換
129
+ return {"start": annotation_body["data"]["begin"], "end": annotation_body["data"]["end"], "_type": "Time"}
130
+ case _:
131
+ # 塗りつぶしや分類など
132
+ return {"x": 0, "y": 0, "_type": "Point"}
133
+
134
+
54
135
  class PutCommentMain(CommandLineWithConfirm):
55
136
  def __init__(self, service: annofabapi.Resource, project_id: str, comment_type: CommentType, all_yes: bool = False) -> None: # noqa: FBT001, FBT002
56
137
  self.service = service
@@ -60,30 +141,68 @@ class PutCommentMain(CommandLineWithConfirm):
60
141
  self.comment_type = comment_type
61
142
  self.comment_type_name = get_comment_type_name(comment_type)
62
143
 
144
+ # プロジェクト情報を取得
145
+ project, _ = self.service.api.get_project(self.project_id)
146
+ self.input_data_type = InputDataType(project["input_data_type"])
147
+
148
+ # アノテーション仕様を取得
149
+ annotation_specs, _ = self.service.api.get_annotation_specs(self.project_id, query_params={"v": "3"})
150
+ self.dict_label_id_annotation_type = {label["label_id"]: label["annotation_type"] for label in annotation_specs["labels"]}
151
+
63
152
  CommandLineWithConfirm.__init__(self, all_yes)
64
153
 
65
154
  def _create_request_body(self, task: dict[str, Any], input_data_id: str, comments: list[AddedComment]) -> list[dict[str, Any]]:
66
155
  """batch_update_comments に渡すリクエストボディを作成する。"""
156
+ task_id = task["task_id"]
157
+
158
+ # annotation_idが指定されているがdataがNoneのコメントがあるか確認
159
+ need_annotation_data = any(c.annotation_id is not None and c.data is None for c in comments)
160
+
161
+ dict_annotation_id_label_id: dict[str, str] = {}
162
+ dict_annotation_id_data: dict[str, dict[str, Any]] = {}
67
163
 
68
- def _create_dict_annotation_id() -> dict[str, str]:
69
- content, _ = self.service.api.get_editor_annotation(self.project_id, task["task_id"], input_data_id, query_params={"v": "2"})
70
- details = content["details"]
71
- return {e["annotation_id"]: e["label_id"] for e in details}
164
+ if need_annotation_data:
165
+ # アノテーション詳細を取得
166
+ editor_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id, query_params={"v": "2"})
167
+ details = editor_annotation["details"]
72
168
 
73
- dict_annotation_id_label_id = _create_dict_annotation_id()
169
+ for detail in details:
170
+ annotation_id = detail["annotation_id"]
171
+ label_id = detail["label_id"]
172
+ dict_annotation_id_label_id[annotation_id] = label_id
173
+
174
+ # annotation_typeを取得
175
+ if label_id in self.dict_label_id_annotation_type:
176
+ # アノテーション仕様に存在しないラベルを使っているアノテーション(アノテーションを作成してから仕様を変更した場合)もあるので、判定する
177
+ annotation_type = self.dict_label_id_annotation_type[label_id]
178
+ dict_annotation_id_data[annotation_id] = convert_annotation_body_to_inspection_data(detail["body"], annotation_type, input_data_type=self.input_data_type)
179
+ else:
180
+ # annotation_idからlabel_idを取得するためだけにAPIを呼ぶ
181
+ editor_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id, query_params={"v": "2"})
182
+ details = editor_annotation["details"]
183
+ dict_annotation_id_label_id = {e["annotation_id"]: e["label_id"] for e in details}
74
184
 
75
- def _convert(comment: AddedComment) -> dict[str, Any]:
185
+ def _convert(comment: AddedComment) -> dict[str, Any] | None:
186
+ data = comment.data
187
+ annotation_id = comment.annotation_id
188
+
189
+ # dataがNoneでannotation_idが指定されている場合、dataを補完
190
+ if data is None:
191
+ assert annotation_id is not None
192
+ data = dict_annotation_id_data[annotation_id]
193
+
194
+ assert data is not None
76
195
  return {
77
- "comment_id": str(uuid.uuid4()),
196
+ "comment_id": comment.comment_id if comment.comment_id is not None else str(uuid.uuid4()),
78
197
  "phase": task["phase"],
79
198
  "phase_stage": task["phase_stage"],
80
199
  "account_id": self.service.api.account_id,
81
200
  "comment_type": self.comment_type.value,
82
201
  "comment": comment.comment,
83
202
  "comment_node": {
84
- "data": comment.data,
85
- "annotation_id": comment.annotation_id,
86
- "label_id": dict_annotation_id_label_id.get(comment.annotation_id) if comment.annotation_id is not None else None,
203
+ "data": data,
204
+ "annotation_id": annotation_id,
205
+ "label_id": dict_annotation_id_label_id.get(annotation_id) if annotation_id is not None else None,
87
206
  "status": "open",
88
207
  "_type": "Root",
89
208
  },
@@ -91,7 +210,8 @@ class PutCommentMain(CommandLineWithConfirm):
91
210
  "_type": "Put",
92
211
  }
93
212
 
94
- return [_convert(e) for e in comments]
213
+ converted_comments = [_convert(e) for e in comments]
214
+ return [c for c in converted_comments if c is not None]
95
215
 
96
216
  def change_to_working_status(self, project_id: str, task: dict[str, Any]) -> dict[str, Any]:
97
217
  """
@@ -129,8 +249,8 @@ class PutCommentMain(CommandLineWithConfirm):
129
249
  logger.warning(f"task_id='{task_id}' :: フェーズが検査/受入でないため検査コメントを付与できません。 :: task_phase='{task['phase']}'")
130
250
  return False
131
251
 
132
- if task["status"] not in [TaskStatus.NOT_STARTED.value, TaskStatus.WORKING.value, TaskStatus.BREAK.value]:
133
- logger.warning(f"task_id='{task_id}' :: タスクの状態が未着手,作業中,休憩中 以外の状態なので、コメントを付与できません。 :: task_status='{task['status']}'")
252
+ if task["status"] not in [TaskStatus.NOT_STARTED.value, TaskStatus.BREAK.value]:
253
+ logger.warning(f"task_id='{task_id}' :: タスクの状態が未着手,休憩中 以外の状態なので、コメントを付与できません。 :: task_status='{task['status']}'")
134
254
  return False
135
255
  return True
136
256
 
@@ -139,7 +259,7 @@ class PutCommentMain(CommandLineWithConfirm):
139
259
  task_id: str,
140
260
  comments_for_task: AddedCommentsForTask,
141
261
  task_index: int | None = None,
142
- ) -> int:
262
+ ) -> tuple[int, int]:
143
263
  """
144
264
  タスクにコメントを付与します。
145
265
 
@@ -149,28 +269,29 @@ class PutCommentMain(CommandLineWithConfirm):
149
269
  task_index: タスクの連番
150
270
 
151
271
  Returns:
152
- コメントを付与した入力データの個数
272
+ (コメントを付与した入力データの個数, 付与したコメントの個数)のタプル
153
273
  """
154
274
  logging_prefix = f"{task_index + 1} 件目" if task_index is not None else ""
155
275
 
156
276
  task = self.service.wrapper.get_task_or_none(self.project_id, task_id)
157
277
  if task is None:
158
278
  logger.warning(f"{logging_prefix} :: task_id='{task_id}' のタスクは存在しないので、スキップします。")
159
- return 0
279
+ return (0, 0)
160
280
 
161
281
  logger.debug(f"{logging_prefix} : task_id='{task['task_id']}', status='{task['status']}', phase='{task['phase']}'")
162
282
 
163
283
  if not self._can_add_comment(
164
284
  task=task,
165
285
  ):
166
- return 0
286
+ return (0, 0)
167
287
 
168
288
  if not self.confirm_processing(f"task_id='{task_id}' のタスクに{self.comment_type_name}を付与しますか?"):
169
- return 0
289
+ return (0, 0)
170
290
 
171
291
  # コメントを付与するには作業中状態にする必要がある
172
292
  changed_task = self.change_to_working_status(self.project_id, task)
173
- added_comments_count = 0
293
+ added_input_data_count = 0
294
+ added_comment_count = 0
174
295
  for input_data_id, comments in comments_for_task.items():
175
296
  if input_data_id not in task["input_data_id_list"]:
176
297
  logger.warning(f"{logging_prefix} :: task_id='{task_id}'のタスクに input_data_id='{input_data_id}'の入力データは存在しません。")
@@ -180,7 +301,8 @@ class PutCommentMain(CommandLineWithConfirm):
180
301
  if len(comments) > 0:
181
302
  request_body = self._create_request_body(task=changed_task, input_data_id=input_data_id, comments=comments)
182
303
  self.service.api.batch_update_comments(self.project_id, task_id, input_data_id, request_body=request_body)
183
- added_comments_count += 1
304
+ added_input_data_count += 1
305
+ added_comment_count += len(comments)
184
306
  logger.debug(f"{logging_prefix} :: task_id='{task_id}', input_data_id='{input_data_id}' :: {len(comments)}件のコメントを付与しました。")
185
307
  except Exception: # pylint: disable=broad-except
186
308
  logger.warning(
@@ -194,12 +316,12 @@ class PutCommentMain(CommandLineWithConfirm):
194
316
  self.service.wrapper.change_task_operator(self.project_id, task_id, task["account_id"])
195
317
  logger.debug(f"{logging_prefix} :: task_id='{task_id}' :: 担当者を元のユーザ( account_id='{task['account_id']}')に戻しました。")
196
318
 
197
- return added_comments_count
319
+ return (added_input_data_count, added_comment_count)
198
320
 
199
321
  def add_comments_for_task_wrapper(
200
322
  self,
201
323
  tpl: tuple[int, tuple[str, AddedCommentsForTask]],
202
- ) -> int:
324
+ ) -> tuple[int, int]:
203
325
  task_index, (task_id, comments_for_task) = tpl
204
326
  return self.add_comments_for_task(task_id=task_id, comments_for_task=comments_for_task, task_index=task_index)
205
327
 
@@ -208,30 +330,45 @@ class PutCommentMain(CommandLineWithConfirm):
208
330
  comments_for_task_list: AddedComments,
209
331
  parallelism: int | None = None,
210
332
  ) -> None:
211
- comments_count = sum(len(e) for e in comments_for_task_list.values())
212
- logger.info(f"{self.comment_type_name}を付与するタスク数: {len(comments_for_task_list)}, {self.comment_type_name}を付与する入力データ数: {comments_count}")
333
+ tasks_count = len(comments_for_task_list)
334
+ input_data_count = sum(len(e) for e in comments_for_task_list.values())
335
+ total_comment_count = sum(len(comments) for comments_for_task in comments_for_task_list.values() for comments in comments_for_task.values())
336
+ logger.info(
337
+ f"{self.comment_type_name}を付与するタスク数: {tasks_count}, {self.comment_type_name}を付与する入力データ数: {input_data_count}, {self.comment_type_name}の総数: {total_comment_count}"
338
+ )
213
339
 
214
340
  if parallelism is not None:
215
341
  with multiprocessing.Pool(parallelism) as pool:
216
- result_bool_list = pool.map(self.add_comments_for_task_wrapper, enumerate(comments_for_task_list.items()))
217
- added_comments_count = sum(e for e in result_bool_list)
342
+ result_list = pool.map(self.add_comments_for_task_wrapper, enumerate(comments_for_task_list.items()))
343
+ added_input_data_count = sum(e[0] for e in result_list)
344
+ added_comment_count = sum(e[1] for e in result_list)
345
+ succeeded_tasks_count = sum(1 for e in result_list if e[0] > 0)
218
346
 
219
347
  else:
220
348
  # 逐次処理
221
- added_comments_count = 0
349
+ added_input_data_count = 0
350
+ added_comment_count = 0
351
+ succeeded_tasks_count = 0
222
352
  for task_index, (task_id, comments_for_task) in enumerate(comments_for_task_list.items()):
223
353
  try:
224
- result = self.add_comments_for_task(
354
+ result_input_data, result_comment = self.add_comments_for_task(
225
355
  task_id=task_id,
226
356
  comments_for_task=comments_for_task,
227
357
  task_index=task_index,
228
358
  )
229
- added_comments_count += result
359
+ added_input_data_count += result_input_data
360
+ added_comment_count += result_comment
361
+ if result_input_data > 0:
362
+ succeeded_tasks_count += 1
230
363
  except Exception: # pylint: disable=broad-except
231
364
  logger.warning(f"task_id='{task_id}' :: コメントの付与に失敗しました。", exc_info=True)
232
365
  continue
233
366
 
234
- logger.info(f"{added_comments_count} / {comments_count} 件の入力データに{self.comment_type_name}を付与しました。")
367
+ logger.info(
368
+ f"{succeeded_tasks_count} / {tasks_count} 件のタスク, "
369
+ f"{added_input_data_count} / {input_data_count} 件の入力データ, "
370
+ f"{added_comment_count} / {total_comment_count} 件の{self.comment_type_name}を付与しました。"
371
+ )
235
372
 
236
373
 
237
374
  def convert_cli_comments(dict_comments: dict[str, Any], *, comment_type: CommentType) -> AddedComments:
@@ -248,7 +385,7 @@ def convert_cli_comments(dict_comments: dict[str, Any], *, comment_type: Comment
248
385
  comment: str
249
386
  """コメントの中身"""
250
387
 
251
- data: dict[str, Any]
388
+ data: dict[str, Any] | None = None
252
389
  """コメントを付与する位置や区間"""
253
390
 
254
391
  annotation_id: str | None = None
@@ -257,6 +394,9 @@ def convert_cli_comments(dict_comments: dict[str, Any], *, comment_type: Comment
257
394
  phrases: list[str] | None = None
258
395
  """参照している定型指摘ID"""
259
396
 
397
+ comment_id: str | None = None
398
+ """コメントID。省略時はUUIDv4が自動生成される。"""
399
+
260
400
  @dataclass
261
401
  class AddedOnholdComment(DataClassJsonMixin):
262
402
  """
@@ -269,13 +409,16 @@ def convert_cli_comments(dict_comments: dict[str, Any], *, comment_type: Comment
269
409
  annotation_id: str | None = None
270
410
  """コメントに紐付けるアノテーションID"""
271
411
 
412
+ comment_id: str | None = None
413
+ """コメントID。省略時はUUIDv4が自動生成される。"""
414
+
272
415
  def convert_inspection_comment(comment: dict[str, Any]) -> AddedComment:
273
416
  tmp = AddedInspectionComment.from_dict(comment)
274
- return AddedComment(comment=tmp.comment, data=tmp.data, annotation_id=tmp.annotation_id, phrases=tmp.phrases)
417
+ return AddedComment(comment=tmp.comment, data=tmp.data, annotation_id=tmp.annotation_id, phrases=tmp.phrases, comment_id=tmp.comment_id)
275
418
 
276
419
  def convert_onhold_comment(comment: dict[str, Any]) -> AddedComment:
277
420
  tmp = AddedOnholdComment.from_dict(comment)
278
- return AddedComment(comment=tmp.comment, annotation_id=tmp.annotation_id, data=None, phrases=None)
421
+ return AddedComment(comment=tmp.comment, annotation_id=tmp.annotation_id, data=None, phrases=None, comment_id=tmp.comment_id)
279
422
 
280
423
  if comment_type == CommentType.INSPECTION:
281
424
  func_convert = convert_inspection_comment
@@ -31,6 +31,9 @@ class AddedSimpleComment:
31
31
  phrases: list[str] | None = None
32
32
  """参照している定型指摘ID"""
33
33
 
34
+ comment_id: str | None = None
35
+ """コメントID。省略時はUUIDv4が自動生成される。"""
36
+
34
37
 
35
38
  class PutCommentSimplyMain(CommandLineWithConfirm):
36
39
  def __init__(self, service: annofabapi.Resource, project_id: str, comment_type: CommentType, all_yes: bool = False) -> None: # noqa: FBT001, FBT002
@@ -49,7 +52,7 @@ class PutCommentSimplyMain(CommandLineWithConfirm):
49
52
  def _convert(comment: AddedSimpleComment) -> dict[str, Any]:
50
53
  return {
51
54
  "comment": comment.comment,
52
- "comment_id": str(uuid.uuid4()),
55
+ "comment_id": comment.comment_id if comment.comment_id is not None else str(uuid.uuid4()),
53
56
  "phase": task["phase"],
54
57
  "phase_stage": task["phase_stage"],
55
58
  "comment_type": self.comment_type.value,
@@ -90,11 +93,11 @@ class PutCommentSimplyMain(CommandLineWithConfirm):
90
93
 
91
94
  if self.comment_type == CommentType.INSPECTION: # noqa: SIM102
92
95
  if task["phase"] == TaskPhase.ANNOTATION.value:
93
- logger.warning(f"task_id='{task_id}': 教師付フェーズなので、検査コメントを付与できません。")
96
+ logger.warning(f"task_id='{task_id}' :: フェーズが検査/受入でないため検査コメントを付与できません。 :: task_phase='{task['phase']}'")
94
97
  return False
95
98
 
96
- if task["status"] not in [TaskStatus.NOT_STARTED.value, TaskStatus.WORKING.value, TaskStatus.BREAK.value]:
97
- logger.warning(f"task_id='{task_id}' : タスクの状態が未着手,作業中,休憩中 以外の状態なので、コメントを付与できません。(task_status='{task['status']}'")
99
+ if task["status"] not in [TaskStatus.NOT_STARTED.value, TaskStatus.BREAK.value]:
100
+ logger.warning(f"task_id='{task_id}' :: タスクの状態が未着手,休憩中 以外の状態なので、コメントを付与できません。 :: task_status='{task['status']}'")
98
101
  return False
99
102
  return True
100
103
 
@@ -68,7 +68,11 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
68
68
  parser.add_argument(
69
69
  "--json",
70
70
  type=str,
71
- help=(f"付与する検査コメントの内容をJSON形式で指定してください。``file://`` を先頭に付けると、JSON形式のファイルを指定できます。\n\n(ex) ``{json.dumps(SAMPLE_JSON, ensure_ascii=False)}``"),
71
+ help=(
72
+ f"付与する検査コメントの内容をJSON形式で指定してください。``file://`` を先頭に付けると、JSON形式のファイルを指定できます。\n\n"
73
+ f"各コメントには ``comment_id`` を指定することができます。省略した場合は自動的にUUIDv4が生成されます。\n\n"
74
+ f"(ex) ``{json.dumps(SAMPLE_JSON, ensure_ascii=False)}``"
75
+ ),
72
76
  )
73
77
 
74
78
  parser.add_argument(
@@ -69,7 +69,11 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
69
69
  parser.add_argument(
70
70
  "--json",
71
71
  type=str,
72
- help=(f"付与する保留コメントの情報をJSON形式で指定してください。``file://`` を先頭に付けると、JSON形式のファイルを指定できます。\n\n(ex) ``{json.dumps(SAMPLE_JSON, ensure_ascii=False)}``"),
72
+ help=(
73
+ f"付与する保留コメントの情報をJSON形式で指定してください。``file://`` を先頭に付けると、JSON形式のファイルを指定できます。\n\n"
74
+ f"各コメントには ``comment_id`` を指定することができます。省略した場合は自動的にUUIDv4が生成されます。\n\n"
75
+ f"(ex) ``{json.dumps(SAMPLE_JSON, ensure_ascii=False)}``"
76
+ ),
73
77
  )
74
78
 
75
79
  parser.add_argument(
@@ -182,7 +182,7 @@ class DrawingAnnotationForOneImage:
182
182
  image.save(output_file)
183
183
 
184
184
  elif image_size is not None:
185
- image = Image.new("RGBA", image_size, color="black")
185
+ image = Image.new("RGBA", image_size, color="black") # type: ignore[assignment]
186
186
  draw = ImageDraw.Draw(image)
187
187
  self._draw_annotations(draw, parser)
188
188
  output_file.parent.mkdir(parents=True, exist_ok=True)
@@ -38,9 +38,9 @@ def _create_dict_tasks_by_input_data_id(task_list: list[dict[str, Any]]) -> dict
38
38
  for input_data_index, input_data_id in enumerate(task["input_data_id_list"]):
39
39
  new_task = {
40
40
  "task_id": task["task_id"],
41
- "task_status": task["status"],
42
41
  "task_phase": task["phase"],
43
42
  "task_phase_stage": task["phase_stage"],
43
+ "task_status": task["status"],
44
44
  "frame_no": input_data_index + 1,
45
45
  }
46
46
  result[input_data_id].append(new_task)
@@ -70,7 +70,7 @@ def create_df_input_data_with_merged_task(input_data_list: list[dict[str, Any]])
70
70
  """
71
71
 
72
72
  def get_columns(columns: pandas.Index) -> list[str]:
73
- task_columns = ["task_id", "task_status", "task_phase", "task_phase_stage", "frame_no"]
73
+ task_columns = ["task_id", "task_phase", "task_phase_stage", "task_status", "frame_no"]
74
74
  new_columns = columns.drop(task_columns)
75
75
  return list(new_columns) + task_columns
76
76
 
@@ -93,7 +93,7 @@ def create_df_input_data_with_merged_task(input_data_list: list[dict[str, Any]])
93
93
  # ネストしたオブジェクトを`system_metadata.input_duration`のような列名でアクセスできるようにするため
94
94
  df_input_data = pandas.json_normalize(new_input_data_list)
95
95
 
96
- for column in ["task_id", "task_status", "task_phase", "task_phase_stage", "frame_no"]:
96
+ for column in ["task_id", "task_phase", "task_phase_stage", "task_status", "frame_no"]:
97
97
  if column not in df_input_data.columns:
98
98
  df_input_data[column] = pandas.NA
99
99
 
@@ -31,7 +31,7 @@ class CopyInstruction(CommandLine):
31
31
  @staticmethod
32
32
  def get_instruction_image_id_from_url(url: str) -> str:
33
33
  # URL Queryを除いたURLを取得する
34
- url_without_query = url.split("?")[0]
34
+ url_without_query = url.split("?", maxsplit=1)[0]
35
35
  return url_without_query.split("/")[-1]
36
36
 
37
37
  @staticmethod
@@ -34,7 +34,7 @@ def get_bin_edges(min_value: float, max_value: float, bin_width: float) -> numpy
34
34
  # stop引数に、`bin_width*2`を指定している理由:
35
35
  # 引数が小数のときは`len(bin_edges)``期待通りにならないときがあるので、ビンの数を少し増やしている
36
36
  # https://qiita.com/yuji38kwmt/items/ff00f3cb9083567d083f
37
- bin_edges = numpy.arange(start=min_value, stop=max_value + bin_width * 2, step=bin_width)
37
+ bin_edges = numpy.arange(start=min_value, stop=max_value + bin_width * 2, step=bin_width) # type: ignore[call-overload]
38
38
  return bin_edges
39
39
 
40
40
 
@@ -573,9 +573,9 @@ class AttributeCountCsv:
573
573
  basic_columns = [
574
574
  ("project_id", "", ""),
575
575
  ("task_id", "", ""),
576
- ("task_status", "", ""),
577
576
  ("task_phase", "", ""),
578
577
  ("task_phase_stage", "", ""),
578
+ ("task_status", "", ""),
579
579
  ("input_data_count", "", ""),
580
580
  ("annotation_count", "", ""),
581
581
  ]
@@ -622,9 +622,9 @@ class AttributeCountCsv:
622
622
  basic_columns = [
623
623
  ("project_id", "", ""),
624
624
  ("task_id", "", ""),
625
- ("task_status", "", ""),
626
625
  ("task_phase", "", ""),
627
626
  ("task_phase_stage", "", ""),
627
+ ("task_status", "", ""),
628
628
  ("input_data_id", "", ""),
629
629
  ("input_data_name", "", ""),
630
630
  ("frame_no", "", ""),
@@ -708,9 +708,9 @@ class LabelCountCsv:
708
708
  basic_columns = [
709
709
  "project_id",
710
710
  "task_id",
711
- "task_status",
712
711
  "task_phase",
713
712
  "task_phase_stage",
713
+ "task_status",
714
714
  "input_data_count",
715
715
  "annotation_count",
716
716
  ]
@@ -757,9 +757,9 @@ class LabelCountCsv:
757
757
  basic_columns = [
758
758
  "project_id",
759
759
  "task_id",
760
- "task_status",
761
760
  "task_phase",
762
761
  "task_phase_stage",
762
+ "task_status",
763
763
  "input_data_id",
764
764
  "input_data_name",
765
765
  "frame_no",
@@ -109,9 +109,9 @@ class AnnotationDuration(DataClassJsonMixin):
109
109
 
110
110
  project_id: str
111
111
  task_id: str
112
- task_status: TaskStatus
113
112
  task_phase: TaskPhase
114
113
  task_phase_stage: int
114
+ task_status: TaskStatus
115
115
 
116
116
  input_data_id: str
117
117
  input_data_name: str
@@ -37,7 +37,7 @@ def get_video_duration_list(task_list: list[dict[str, Any]], input_data_list: li
37
37
  result = []
38
38
  for task in task_list:
39
39
  task_id = task["task_id"]
40
- elm = {"project_id": task["project_id"], "task_id": task_id, "task_status": task["status"], "task_phase": task["phase"], "task_phase_stage": task["phase_stage"]}
40
+ elm = {"project_id": task["project_id"], "task_id": task_id, "task_phase": task["phase"], "task_phase_stage": task["phase_stage"], "task_status": task["status"]}
41
41
  input_data_id_list = task["input_data_id_list"]
42
42
  assert len(input_data_id_list) == 1, f"task_id='{task_id}'には複数の入力データが含まれています。"
43
43
  input_data_id = input_data_id_list[0]
@@ -88,7 +88,7 @@ class ChangeOperatorMain:
88
88
 
89
89
  if task.status == TaskStatus.ON_HOLD and not self.include_on_hold:
90
90
  logger.warning(
91
- f"{logging_prefix} :: task_id='{task_id}' :: タスクが保留中状態なので、担当者を変更できません。保留中状態のタスクの担当者も変更する場合は、'--include_on_hold'を指定してください。"
91
+ f"{logging_prefix} :: task_id='{task_id}' :: タスクが保留中状態なので、担当者を変更できません。保留中状態のタスクの担当者も変更する場合は、'--include_on_hold_task'を指定してください。"
92
92
  )
93
93
  return False
94
94
 
@@ -222,7 +222,7 @@ class ChangeOperator(CommandLine):
222
222
  project_id = args.project_id
223
223
  super().validate_project(project_id, [ProjectMemberRole.OWNER, ProjectMemberRole.ACCEPTER])
224
224
 
225
- main_obj = ChangeOperatorMain(self.service, all_yes=self.all_yes, include_on_hold=args.include_on_hold)
225
+ main_obj = ChangeOperatorMain(self.service, all_yes=self.all_yes, include_on_hold=args.include_on_hold_task)
226
226
  main_obj.change_operator(
227
227
  project_id,
228
228
  task_id_list=task_id_list,
@@ -253,7 +253,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
253
253
  argument_parser.add_task_query()
254
254
 
255
255
  parser.add_argument(
256
- "--include_on_hold",
256
+ "--include_on_hold_task",
257
257
  action="store_true",
258
258
  help="指定した場合、保留中のタスクの担当者も変更します。指定しない場合、保留中のタスクはスキップされます。",
259
259
  )
@@ -179,9 +179,11 @@ class CompleteTasksMain(CommandLineWithConfirm):
179
179
  # 教師付フェーズになった日時より、後に付与された返信コメントを取得する
180
180
  answered_comment = first_true(
181
181
  comment_list,
182
- pred=lambda e: e["comment_node"]["_type"] == "Reply"
183
- and e["comment_node"]["root_comment_id"] == parent_comment_id
184
- and dateutil.parser.parse(e["created_datetime"]) >= dateutil.parser.parse(task_started_datetime),
182
+ pred=lambda e: (
183
+ e["comment_node"]["_type"] == "Reply"
184
+ and e["comment_node"]["root_comment_id"] == parent_comment_id
185
+ and dateutil.parser.parse(e["created_datetime"]) >= dateutil.parser.parse(task_started_datetime)
186
+ ),
185
187
  )
186
188
  return answered_comment is not None
187
189
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: annofabcli
3
- Version: 1.114.5
3
+ Version: 1.114.7
4
4
  Summary: Utility Command Line Interface for AnnoFab
5
5
  Author: Kurusugawa Computer Inc.
6
6
  License: MIT