annofabcli 1.105.1__py3-none-any.whl → 1.106.1__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 (28) hide show
  1. annofabcli/__main__.py +3 -1
  2. annofabcli/annotation/change_annotation_attributes_per_annotation.py +7 -3
  3. annofabcli/annotation/dump_annotation.py +26 -11
  4. annofabcli/common/cli.py +0 -27
  5. annofabcli/common/pandas.py +4 -1
  6. annofabcli/project/copy_project.py +3 -1
  7. annofabcli/statistics/list_annotation_area.py +6 -1
  8. annofabcli/statistics/list_annotation_attribute.py +4 -0
  9. annofabcli/statistics/list_annotation_attribute_filled_count.py +5 -0
  10. annofabcli/statistics/list_annotation_count.py +7 -0
  11. annofabcli/statistics/list_annotation_duration.py +7 -0
  12. annofabcli/statistics/visualization/dataframe/cumulative_productivity.py +0 -4
  13. annofabcli/statistics/visualization/dataframe/task.py +21 -1
  14. annofabcli/statistics/visualization/dataframe/task_worktime_by_phase_user.py +13 -0
  15. annofabcli/statistics/visualization/dataframe/user_performance.py +6 -0
  16. annofabcli/statistics/visualization/dataframe/whole_productivity_per_date.py +21 -4
  17. annofabcli/statistics/visualization/dataframe/worktime_per_date.py +14 -8
  18. annofabcli/statistics/visualization/filtering_query.py +5 -1
  19. annofabcli/statistics/visualization/model.py +32 -0
  20. annofabcli/statistics/visualization/project_dir.py +7 -1
  21. annofabcli/statistics/visualize_statistics.py +3 -9
  22. annofabcli/statistics/visualize_video_duration.py +1 -1
  23. annofabcli/task/list_tasks_added_task_history.py +28 -7
  24. {annofabcli-1.105.1.dist-info → annofabcli-1.106.1.dist-info}/METADATA +1 -1
  25. {annofabcli-1.105.1.dist-info → annofabcli-1.106.1.dist-info}/RECORD +28 -28
  26. {annofabcli-1.105.1.dist-info → annofabcli-1.106.1.dist-info}/WHEEL +0 -0
  27. {annofabcli-1.105.1.dist-info → annofabcli-1.106.1.dist-info}/entry_points.txt +0 -0
  28. {annofabcli-1.105.1.dist-info → annofabcli-1.106.1.dist-info}/licenses/LICENSE +0 -0
annofabcli/__main__.py CHANGED
@@ -38,7 +38,9 @@ def warn_pandas_copy_on_write() -> None:
38
38
  pandas 3.0で予期しない挙動になるのを防ぐため。
39
39
  https://pandas.pydata.org/docs/user_guide/copy_on_write.html
40
40
  """
41
- major, minor, _ = pandas.__version__.split(".")
41
+ tmp = pandas.__version__.split(".")
42
+ major = tmp[0]
43
+ minor = tmp[1]
42
44
  if int(major) >= 2 and int(minor) >= 2:
43
45
  pandas.options.mode.copy_on_write = "warn"
44
46
 
@@ -178,7 +178,8 @@ class ChangeAnnotationAttributesPerAnnotationMain(CommandLineWithConfirm):
178
178
  anno_list: 各アノテーションの変更内容リスト
179
179
  """
180
180
  changed_task_count = 0
181
- failed_to_change_annotation_count = 0
181
+ total_failed_to_change_annotation_count = 0
182
+ total_succeed_to_change_annotation_count = 0
182
183
  annotation_list_per_task_id_input_data_id = get_annotation_list_per_task_id_input_data_id(anno_list)
183
184
 
184
185
  total_task_count = len(annotation_list_per_task_id_input_data_id)
@@ -186,13 +187,16 @@ class ChangeAnnotationAttributesPerAnnotationMain(CommandLineWithConfirm):
186
187
 
187
188
  for task_id, input_data_dict in annotation_list_per_task_id_input_data_id.items():
188
189
  is_changeable_task, succeed_to_change_annotation_count, failed_to_change_annotation_count = self.change_annotation_attributes_for_task(task_id, input_data_dict)
190
+ total_succeed_to_change_annotation_count += succeed_to_change_annotation_count
191
+ total_failed_to_change_annotation_count += failed_to_change_annotation_count
192
+
189
193
  if is_changeable_task:
190
194
  changed_task_count += 1
191
195
 
192
196
  logger.info(
193
- f"{succeed_to_change_annotation_count}/{total_annotation_count} 件のアノテーションの属性値を変更しました。 :: "
197
+ f"{total_succeed_to_change_annotation_count}/{total_annotation_count} 件のアノテーションの属性値を変更しました。 :: "
194
198
  f"アノテーションが変更されたタスク数は {changed_task_count}/{total_task_count} 件です。"
195
- f"{failed_to_change_annotation_count} 件のアノテーションは変更できませんでした。"
199
+ f"{total_failed_to_change_annotation_count} 件のアノテーションは変更できませんでした。"
196
200
  )
197
201
 
198
202
 
@@ -63,12 +63,12 @@ class DumpAnnotationMain:
63
63
  outer_file_path = outer_dir / f"{annotation_id}"
64
64
  self.service.wrapper.download(detail["url"], outer_file_path)
65
65
 
66
- def dump_annotation_for_input_data(self, task_id: str, input_data_id: str, task_dir: Path) -> None:
67
- editor_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id, query_params={"v": "2"})
66
+ def dump_annotation_for_input_data(self, task_id: str, input_data_id: str, task_dir: Path, *, task_history_id: Optional[str] = None) -> None:
67
+ editor_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id, query_params={"v": "2", "task_history_id": task_history_id})
68
68
  json_path = task_dir / f"{input_data_id}.json"
69
69
  self.dump_editor_annotation(editor_annotation=editor_annotation, json_path=json_path)
70
70
 
71
- def dump_annotation_for_task(self, task_id: str, output_dir: Path, *, task_index: Optional[int] = None) -> bool:
71
+ def dump_annotation_for_task(self, task_id: str, output_dir: Path, *, task_index: Optional[int] = None, task_history_index: Optional[int] = None) -> bool:
72
72
  """
73
73
  タスク配下のアノテーションをファイルに保存する。
74
74
 
@@ -85,15 +85,23 @@ class DumpAnnotationMain:
85
85
  logger.warning(f"task_id = '{task_id}' のタスクは存在しません。スキップします。")
86
86
  return False
87
87
 
88
+ task_history_id: Optional[str] = None
89
+ if task_history_index is not None:
90
+ task_histories, _ = self.service.api.get_task_histories(self.project_id, task_id)
91
+ if task_history_index >= len(task_histories):
92
+ logger.warning(f"task_id='{task_id}' :: task_history_index='{task_history_index}'のタスク履歴は存在しません。タスク履歴は{len(task_histories)}件です。スキップします。")
93
+ return False
94
+ task_history_id = task_histories[task_history_index]["task_history_id"]
95
+
88
96
  input_data_id_list = task["input_data_id_list"]
89
97
  task_dir = output_dir / task_id
90
98
  task_dir.mkdir(exist_ok=True, parents=True)
91
- logger.debug(f"{logger_prefix}task_id = '{task_id}' のアノテーション情報を '{task_dir}' ディレクトリに保存します。")
99
+ logger.debug(f"{logger_prefix}task_id = '{task_id}' のアノテーション情報を '{task_dir}' ディレクトリに保存します。 :: task_history_id='{task_history_id}'")
92
100
 
93
101
  is_failure = False
94
102
  for input_data_id in input_data_id_list:
95
103
  try:
96
- self.dump_annotation_for_input_data(task_id, input_data_id, task_dir=task_dir)
104
+ self.dump_annotation_for_input_data(task_id, input_data_id, task_dir=task_dir, task_history_id=task_history_id)
97
105
  except Exception:
98
106
  logger.warning(f"タスク'{task_id}', 入力データ'{input_data_id}' のアノテーション情報のダンプに失敗しました。", exc_info=True)
99
107
  is_failure = True
@@ -101,15 +109,15 @@ class DumpAnnotationMain:
101
109
 
102
110
  return not is_failure
103
111
 
104
- def dump_annotation_for_task_wrapper(self, tpl: tuple[int, str], output_dir: Path) -> bool:
112
+ def dump_annotation_for_task_wrapper(self, tpl: tuple[int, str], output_dir: Path, *, task_history_index: Optional[int] = None) -> bool:
105
113
  task_index, task_id = tpl
106
114
  try:
107
- return self.dump_annotation_for_task(task_id, output_dir=output_dir, task_index=task_index)
115
+ return self.dump_annotation_for_task(task_id, output_dir=output_dir, task_index=task_index, task_history_index=task_history_index)
108
116
  except Exception: # pylint: disable=broad-except
109
117
  logger.warning(f"タスク'{task_id}'のアノテーション情報のダンプに失敗しました。", exc_info=True)
110
118
  return False
111
119
 
112
- def dump_annotation(self, task_id_list: list[str], output_dir: Path, parallelism: Optional[int] = None): # noqa: ANN201
120
+ def dump_annotation(self, task_id_list: list[str], output_dir: Path, *, task_history_index: Optional[int] = None, parallelism: Optional[int] = None) -> None:
113
121
  project_title = self.facade.get_project_title(self.project_id)
114
122
  logger.info(f"プロジェクト'{project_title}'に対して、タスク{len(task_id_list)} 件のアノテーションをファイルに保存します。")
115
123
 
@@ -118,7 +126,7 @@ class DumpAnnotationMain:
118
126
  success_count = 0
119
127
 
120
128
  if parallelism is not None:
121
- func = functools.partial(self.dump_annotation_for_task_wrapper, output_dir=output_dir)
129
+ func = functools.partial(self.dump_annotation_for_task_wrapper, task_history_index=task_history_index, output_dir=output_dir)
122
130
  with multiprocessing.Pool(parallelism) as pool:
123
131
  result_bool_list = pool.map(func, enumerate(task_id_list))
124
132
  success_count = len([e for e in result_bool_list if e])
@@ -126,7 +134,7 @@ class DumpAnnotationMain:
126
134
  else:
127
135
  for task_index, task_id in enumerate(task_id_list):
128
136
  try:
129
- result = self.dump_annotation_for_task(task_id, output_dir=output_dir, task_index=task_index)
137
+ result = self.dump_annotation_for_task(task_id, output_dir=output_dir, task_history_index=task_history_index, task_index=task_index)
130
138
  if result:
131
139
  success_count += 1
132
140
  except Exception:
@@ -149,7 +157,7 @@ class DumpAnnotation(CommandLine):
149
157
  super().validate_project(project_id, project_member_roles=None)
150
158
 
151
159
  main_obj = DumpAnnotationMain(self.service, project_id)
152
- main_obj.dump_annotation(task_id_list, output_dir=output_dir, parallelism=args.parallelism)
160
+ main_obj.dump_annotation(task_id_list, output_dir=output_dir, parallelism=args.parallelism, task_history_index=args.task_history_index)
153
161
 
154
162
 
155
163
  def main(args: argparse.Namespace) -> None:
@@ -166,6 +174,13 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
166
174
 
167
175
  parser.add_argument("-o", "--output_dir", type=str, required=True, help="出力先ディレクトリのパス")
168
176
 
177
+ parser.add_argument(
178
+ "--task_history_index",
179
+ type=int,
180
+ help="指定したタスク履歴のインデックス(ゼロ始まり)で付与されたアノテーション情報をダンプします。過去のアノテーション結果をダンプする場合に指定します。"
181
+ "ただし、過去のアノテーションデータは30日間しか保持されません。30日より前に更新されたアノテーションをダンプした場合は、アノテーションが0件の状態でダンプされます。",
182
+ )
183
+
169
184
  parser.add_argument(
170
185
  "--parallelism",
171
186
  type=int,
annofabcli/common/cli.py CHANGED
@@ -524,33 +524,6 @@ class CommandLineWithoutWebapi:
524
524
 
525
525
  return yes
526
526
 
527
- def confirm_processing_task(self, task_id: str, confirm_message: str) -> bool:
528
- """
529
- タスクに対して処理するかどうか問い合わせる。
530
- `all_yes`属性も設定する。
531
-
532
- Args:
533
- task_id: 処理するtask_id
534
- confirm_message: 確認メッセージ
535
-
536
- Returns:
537
- Trueならば対象のタスクを処理する。
538
-
539
- """
540
- if self.all_yes:
541
- return True
542
-
543
- yes, all_yes = prompt_yesnoall(confirm_message)
544
-
545
- if not yes:
546
- logger.info(f"task_id = {task_id} をスキップします。")
547
- return False
548
-
549
- if all_yes:
550
- self.all_yes = True
551
-
552
- return True
553
-
554
527
  def print_csv(self, df: pandas.DataFrame) -> None:
555
528
  print_csv(df, output=self.output)
556
529
 
@@ -8,7 +8,10 @@ def get_frequency_of_monthend() -> str:
8
8
 
9
9
  https://github.com/pandas-dev/pandas/issues/9586
10
10
  """
11
- major, minor, _ = pandas.__version__.split(".")
11
+ tmp = pandas.__version__.split(".")
12
+ major = tmp[0]
13
+ minor = tmp[1]
14
+
12
15
  if int(major) >= 2 and int(minor) >= 2:
13
16
  return "ME"
14
17
  return "M"
@@ -83,6 +83,8 @@ class CopyProject(CommandLine):
83
83
 
84
84
  if copied_targets is not None:
85
85
  logger.info(f"コピー対象: {[e.value for e in copied_targets]}")
86
+ else:
87
+ logger.info("コピー対象: None")
86
88
 
87
89
  confirm_message = f"プロジェクト'{src_project_title}'(project_id='{src_project_id}')を、プロジェクト'{dest_title}'(project_id='{dest_project_id}') にコピーしますか?"
88
90
  if not self.confirm_processing(confirm_message):
@@ -111,7 +113,7 @@ class CopyProject(CommandLine):
111
113
  max_job_access=DEFAULT_WAIT_OPTIONS.max_tries,
112
114
  )
113
115
  if result:
114
- logger.info("プロジェクトのコピーが完了しました。")
116
+ logger.info(f"プロジェクト'{src_project_title}'のコピーが完了しました。 :: dest_project_id='{dest_project_id}', dest_project_title='{dest_title}'")
115
117
  else:
116
118
  logger.info("プロジェクトのコピーは実行中 または 失敗しました。")
117
119
 
@@ -64,6 +64,9 @@ class AnnotationAreaInfo(DataClassJsonMixin):
64
64
  input_data_id: str
65
65
  input_data_name: str
66
66
 
67
+ updated_datetime: Optional[str]
68
+ """アノテーションJSONに格納されているアノテーションの更新日時"""
69
+
67
70
  label: str
68
71
  annotation_id: str
69
72
  annotation_area: int
@@ -77,7 +80,7 @@ def calculate_bounding_box_area(data: dict[str, Any]) -> int:
77
80
 
78
81
  def calculate_segmentation_area(outer_file: Path) -> int:
79
82
  nd_array = read_binary_image(outer_file)
80
- return numpy.count_nonzero(nd_array)
83
+ return int(numpy.count_nonzero(nd_array))
81
84
 
82
85
 
83
86
  def get_annotation_area_info_list(parser: SimpleAnnotationParser, simple_annotation: dict[str, Any]) -> list[AnnotationAreaInfo]:
@@ -103,6 +106,7 @@ def get_annotation_area_info_list(parser: SimpleAnnotationParser, simple_annotat
103
106
  label=detail["label"],
104
107
  annotation_id=detail["annotation_id"],
105
108
  annotation_area=annotation_area,
109
+ updated_datetime=simple_annotation["updated_datetime"],
106
110
  )
107
111
  )
108
112
 
@@ -144,6 +148,7 @@ def create_df(
144
148
  "task_phase_stage",
145
149
  "input_data_id",
146
150
  "input_data_name",
151
+ "updated_datetime",
147
152
  "label",
148
153
  "annotation_id",
149
154
  "annotation_area",
@@ -65,6 +65,8 @@ class AnnotationAttribute(pydantic.BaseModel):
65
65
 
66
66
  input_data_id: str
67
67
  input_data_name: str
68
+ updated_datetime: Optional[str]
69
+ """アノテーションJSONに格納されているアノテーションの更新日時"""
68
70
  annotation_id: str
69
71
  label: str
70
72
  attributes: dict[str, Union[str, int, bool]]
@@ -98,6 +100,7 @@ def get_annotation_attribute_list_from_annotation_json(simple_annotation: dict[s
98
100
  label=detail["label"],
99
101
  annotation_id=detail["annotation_id"],
100
102
  attributes=detail["attributes"],
103
+ updated_datetime=simple_annotation["updated_datetime"],
101
104
  )
102
105
  )
103
106
  return result
@@ -150,6 +153,7 @@ def print_annotation_attribute_list_as_csv(annotation_attribute_list: list, outp
150
153
  "task_phase_stage",
151
154
  "input_data_id",
152
155
  "input_data_name",
156
+ "updated_datetime",
153
157
  "annotation_id",
154
158
  "label",
155
159
  ]
@@ -108,6 +108,8 @@ class AnnotationCountByInputData(DataClassJsonMixin, HasAnnotationAttributeCount
108
108
 
109
109
  input_data_id: str
110
110
  input_data_name: str
111
+ updated_datetime: Optional[str]
112
+ """アノテーションJSONに格納されているアノテーションの更新日時"""
111
113
 
112
114
  annotation_attribute_counts: dict[AttributeValueKey, int] = field(
113
115
  metadata=config(
@@ -275,6 +277,7 @@ class ListAnnotationCounterByInputData:
275
277
  input_data_name=simple_annotation["input_data_name"],
276
278
  annotation_attribute_counts=annotation_count_by_attribute,
277
279
  frame_no=frame_no,
280
+ updated_datetime=simple_annotation["updated_datetime"],
278
281
  )
279
282
 
280
283
  def get_annotation_count_list(
@@ -355,6 +358,7 @@ class AnnotationCountCsvByAttribute:
355
358
  ("input_data_id", "", ""),
356
359
  ("input_data_name", "", ""),
357
360
  ("frame_no", "", ""),
361
+ ("updated_datetime", "", ""),
358
362
  ]
359
363
  value_columns = self._value_columns(annotation_count_list, prior_attribute_columns=prior_attribute_columns)
360
364
  return basic_columns + value_columns
@@ -390,6 +394,7 @@ class AnnotationCountCsvByAttribute:
390
394
  ("task_phase_stage", "", ""): c.task_phase_stage,
391
395
  ("input_data_id", "", ""): c.input_data_id,
392
396
  ("input_data_name", "", ""): c.input_data_name,
397
+ ("updated_datetime", "", ""): c.updated_datetime,
393
398
  ("frame_no", "", ""): c.frame_no,
394
399
  }
395
400
  cell.update(c.annotation_attribute_counts) # type: ignore[arg-type]
@@ -133,6 +133,8 @@ class AnnotationCounterByInputData(AnnotationCounter, DataClassJsonMixin):
133
133
 
134
134
  input_data_id: str
135
135
  input_data_name: str
136
+ updated_datetime: Optional[str]
137
+ """アノテーションJSONに格納されているアノテーションの更新日時"""
136
138
  frame_no: Optional[int] = None
137
139
  """アノテーションJSONには含まれていない情報なので、Optionalにする"""
138
140
 
@@ -263,6 +265,7 @@ class ListAnnotationCounterByInputData:
263
265
  task_status=TaskStatus(simple_annotation["task_status"]),
264
266
  input_data_id=simple_annotation["input_data_id"],
265
267
  input_data_name=simple_annotation["input_data_name"],
268
+ updated_datetime=simple_annotation["updated_datetime"],
266
269
  annotation_count=sum(annotation_count_by_label.values()),
267
270
  annotation_count_by_label=annotation_count_by_label,
268
271
  annotation_count_by_attribute=annotation_count_by_attribute,
@@ -520,6 +523,7 @@ class AttributeCountCsv:
520
523
  ("input_data_id", "", ""),
521
524
  ("input_data_name", "", ""),
522
525
  ("frame_no", "", ""),
526
+ ("updated_datetime", "", ""),
523
527
  ("annotation_count", "", ""),
524
528
  ]
525
529
  value_columns = self._value_columns(counter_list, prior_attribute_columns)
@@ -531,6 +535,7 @@ class AttributeCountCsv:
531
535
  ("input_data_id", "", ""): c.input_data_id,
532
536
  ("input_data_name", "", ""): c.input_data_name,
533
537
  ("frame_no", "", ""): c.frame_no,
538
+ ("updated_datetime", "", ""): c.updated_datetime,
534
539
  ("task_id", "", ""): c.task_id,
535
540
  ("task_status", "", ""): c.task_status.value,
536
541
  ("task_phase", "", ""): c.task_phase.value,
@@ -625,6 +630,7 @@ class LabelCountCsv:
625
630
  "input_data_id",
626
631
  "input_data_name",
627
632
  "frame_no",
633
+ "updated_datetime",
628
634
  "annotation_count",
629
635
  ]
630
636
  value_columns = self._value_columns(counter_list, prior_label_columns)
@@ -636,6 +642,7 @@ class LabelCountCsv:
636
642
  "input_data_id": c.input_data_id,
637
643
  "input_data_name": c.input_data_name,
638
644
  "frame_no": c.frame_no,
645
+ "updated_datetime": c.updated_datetime,
639
646
  "task_id": c.task_id,
640
647
  "task_status": c.task_status.value,
641
648
  "task_phase": c.task_phase.value,
@@ -116,6 +116,10 @@ class AnnotationDuration(DataClassJsonMixin):
116
116
 
117
117
  input_data_id: str
118
118
  input_data_name: str
119
+
120
+ updated_datetime: Optional[str]
121
+ """アノテーションJSONに格納されているアノテーションの更新日時"""
122
+
119
123
  video_duration_second: Optional[float]
120
124
  """動画の長さ[秒]"""
121
125
 
@@ -242,6 +246,7 @@ class ListAnnotationDurationByInputData:
242
246
  task_status=TaskStatus(simple_annotation["task_status"]),
243
247
  input_data_id=simple_annotation["input_data_id"],
244
248
  input_data_name=simple_annotation["input_data_name"],
249
+ updated_datetime=simple_annotation["updated_datetime"],
245
250
  video_duration_second=video_duration_second,
246
251
  annotation_duration_second=sum(annotation_duration_by_label.values()),
247
252
  annotation_duration_second_by_label=annotation_duration_by_label,
@@ -434,6 +439,7 @@ class AnnotationDurationCsvByLabel:
434
439
  "task_phase_stage",
435
440
  "input_data_id",
436
441
  "input_data_name",
442
+ "updated_datetime",
437
443
  "video_duration_second",
438
444
  "annotation_duration_second",
439
445
  ]
@@ -456,6 +462,7 @@ class AnnotationDurationCsvByLabel:
456
462
  "task_phase_stage": c.task_phase_stage,
457
463
  "video_duration_second": c.video_duration_second,
458
464
  "annotation_duration_second": c.annotation_duration_second,
465
+ "updated_datetime": c.updated_datetime,
459
466
  }
460
467
  d.update(c.annotation_duration_second_by_label)
461
468
  return d
@@ -62,10 +62,6 @@ def _create_cumulative_dataframe(task_worktime_by_phase_user: TaskWorktimeByPhas
62
62
  # `YYYY-MM-DDThh:mm:ss.sss+09:00`から`YYYY-MM-DD`を取得する
63
63
  # キャストする理由: 全部nanだとfloat型になって、".str"にアクセスできないため
64
64
  df[f"first_{str_phase}_started_date"] = df["started_datetime"].astype("string").str[:10]
65
- # bokeh3.0.3では、string型の列を持つpandas.DataFrameを描画できないため、改めてobject型に戻す
66
- # TODO この問題が解決されたら、削除する
67
- # https://qiita.com/yuji38kwmt/items/b5da6ed521e827620186
68
- df[f"first_{str_phase}_started_date"] = df[f"first_{str_phase}_started_date"].astype("object")
69
65
  return df
70
66
 
71
67
 
@@ -94,6 +94,21 @@ class Task:
94
94
  df["acceptance_worktime_hour"] = 0
95
95
  return Task(df, custom_production_volume_list=self.custom_production_volume_list)
96
96
 
97
+ def to_non_inspection_acceptance(self) -> Task:
98
+ """
99
+ 検査フェーズと受入フェーズの作業時間を0にした新しいインスタンスを生成します。
100
+
101
+ `--task_completion_criteria inspection_reached`を指定したときに利用します。
102
+ この場合、検査/受入フェーズの作業時間を無視して集計する必要があるためです。
103
+
104
+ """
105
+ df = self.df.copy()
106
+ df["first_inspection_worktime_hour"] = 0
107
+ df["first_acceptance_worktime_hour"] = 0
108
+ df["inspection_worktime_hour"] = 0
109
+ df["acceptance_worktime_hour"] = 0
110
+ return Task(df, custom_production_volume_list=self.custom_production_volume_list)
111
+
97
112
  @property
98
113
  def optional_columns(self) -> list[str]:
99
114
  return [
@@ -104,6 +119,9 @@ class Task:
104
119
  "post_rejection_annotation_worktime_hour",
105
120
  "post_rejection_inspection_worktime_hour",
106
121
  "post_rejection_acceptance_worktime_hour",
122
+ # 作業時間に関する内容
123
+ "completed_datetime",
124
+ "operation_updated_datetime",
107
125
  ]
108
126
 
109
127
  @property
@@ -134,7 +152,8 @@ class Task:
134
152
  "first_acceptance_username",
135
153
  "first_acceptance_worktime_hour",
136
154
  "first_acceptance_started_datetime",
137
- # 受入フェーズの日時
155
+ # 初めて~になった日時
156
+ "first_inspection_reached_datetime",
138
157
  "first_acceptance_reached_datetime",
139
158
  "first_acceptance_completed_datetime",
140
159
  # 作業時間に関する内容
@@ -254,6 +273,7 @@ class Task:
254
273
  "first_annotation_user_id",
255
274
  "first_annotation_username",
256
275
  "first_annotation_started_datetime",
276
+ "first_inspection_reached_datetime",
257
277
  "first_inspection_user_id",
258
278
  "first_inspection_username",
259
279
  "first_inspection_started_datetime",
@@ -63,6 +63,19 @@ class TaskWorktimeByPhaseUser:
63
63
  df = df[df["phase"] != TaskPhase.ACCEPTANCE.value]
64
64
  return TaskWorktimeByPhaseUser(df, custom_production_volume_list=self.custom_production_volume_list)
65
65
 
66
+ def to_non_inspection_acceptance(self) -> TaskWorktimeByPhaseUser:
67
+ """
68
+ 検査/受入フェーズの作業時間を0にした新しいインスタンスを生成します。
69
+
70
+ `--task_completion_criteria inspection_reached`を指定したときに利用します。
71
+ この場合、検査/受入フェーズの作業時間を無視して集計する必要があるためです。
72
+
73
+ """
74
+ df = self.df.copy()
75
+ # 教師付フェーズのみ残す
76
+ df = df[df["phase"] == TaskPhase.ANNOTATION.value]
77
+ return TaskWorktimeByPhaseUser(df, custom_production_volume_list=self.custom_production_volume_list)
78
+
66
79
  @property
67
80
  def columns(self) -> list[str]:
68
81
  """
@@ -553,6 +553,12 @@ class UserPerformance:
553
553
  worktime_per_date = worktime_per_date.to_non_acceptance()
554
554
  task_worktime_by_phase_user = task_worktime_by_phase_user.to_non_acceptance()
555
555
 
556
+ elif task_completion_criteria == TaskCompletionCriteria.INSPECTION_REACHED:
557
+ # 検査フェーズに到達したらタスクの作業が完了したとみなす場合、
558
+ # 検査フェーズと受入フェーズの作業時間や生産量は不要な情報なので、検査作業時間と受入作業時間を0にする
559
+ worktime_per_date = worktime_per_date.to_non_inspection_acceptance()
560
+ task_worktime_by_phase_user = task_worktime_by_phase_user.to_non_inspection_acceptance()
561
+
556
562
  # 実際の計測作業時間情報(集計タスクに影響されない作業時間)と実績作業時間を算出する
557
563
  df = cls._create_df_real_worktime(worktime_per_date)
558
564
 
@@ -156,10 +156,12 @@ class WholeProductivityPerCompletedDate:
156
156
  monitored_inspection_worktime_hour
157
157
  monitored_acceptance_worktime_hour
158
158
  """
159
-
160
159
  if task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_REACHED:
161
160
  # 受入フェーズに到達したらタスクの作業が完了したとみなす場合、受入フェーズの作業時間や生産量は不要な情報なので、受入作業時間を0にする
162
161
  worktime_per_date = worktime_per_date.to_non_acceptance()
162
+ if task_completion_criteria == TaskCompletionCriteria.INSPECTION_REACHED:
163
+ # 検査フェーズに到達したらタスクの作業が完了したとみなす場合、検査/受入フェーズの作業時間や生産量は不要な情報なので、検査/受入作業時間を0にする
164
+ worktime_per_date = worktime_per_date.to_non_inspection_acceptance()
163
165
 
164
166
  # 生産量を表す列名
165
167
  production_volume_columns = ["input_data_count", "annotation_count", *[e.value for e in task.custom_production_volume_list]]
@@ -168,6 +170,8 @@ class WholeProductivityPerCompletedDate:
168
170
  datetime_column = "first_acceptance_completed_datetime"
169
171
  elif task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_REACHED:
170
172
  datetime_column = "first_acceptance_reached_datetime"
173
+ elif task_completion_criteria == TaskCompletionCriteria.INSPECTION_REACHED:
174
+ datetime_column = "first_inspection_reached_datetime"
171
175
  else:
172
176
  assert_noreturn(task_completion_criteria)
173
177
 
@@ -271,15 +275,19 @@ class WholeProductivityPerCompletedDate:
271
275
  HTMLページの先頭に付与するdiv要素を生成する。
272
276
  """
273
277
  if self.task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_COMPLETED:
274
- str_task = "受入フェーズ完了状態"
278
+ str_task = "受入フェーズ完了状態になった"
275
279
  elif self.task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_REACHED:
276
- str_task = "受入フェーズ"
280
+ str_task = "受入フェーズに到達した"
281
+ elif self.task_completion_criteria == TaskCompletionCriteria.INSPECTION_REACHED:
282
+ str_task = "検査フェーズに到達した"
277
283
  else:
278
284
  assert_noreturn(self.task_completion_criteria)
279
285
 
280
- elm_list = ["<h4>注意</h4>", f"<p>「X日のタスク数」は、X日に初めて{str_task}になったタスクの個数です。</p>"]
286
+ elm_list = ["<h4>注意</h4>", f"<p>「X日のタスク数」は、X日に初めて{str_task}タスクの個数です。</p>"]
281
287
  if self.task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_REACHED:
282
288
  elm_list.append("<p>タスクが受入フェーズに到達したら作業が完了したとみなしているため、受入作業時間は0にしています。</p>")
289
+ elif self.task_completion_criteria == TaskCompletionCriteria.INSPECTION_REACHED:
290
+ elm_list.append("<p>タスクが検査フェーズに到達したら作業が完了したとみなしているため、検査作業時間と受入作業時間は0にしています。</p>")
283
291
  return Div(text=" ".join(elm_list))
284
292
 
285
293
  def plot(
@@ -865,12 +873,17 @@ class WholeProductivityPerFirstAnnotationStartedDate:
865
873
  if task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_REACHED:
866
874
  # 受入フェーズに到達したらタスクの作業が完了したとみなす場合、受入フェーズの作業時間や生産量は不要な情報なので、受入作業時間を0にする
867
875
  task = task.to_non_acceptance()
876
+ elif task_completion_criteria == TaskCompletionCriteria.INSPECTION_REACHED:
877
+ # 検査フェーズに到達したらタスクの作業が完了したとみなす場合、検査/受入フェーズの作業時間や生産量は不要な情報なので、検査/受入作業時間を0にする
878
+ task = task.to_non_inspection_acceptance()
868
879
 
869
880
  df_task = task.df
870
881
  if task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_COMPLETED:
871
882
  df_sub_task = df_task[df_task["status"] == TaskStatus.COMPLETE.value]
872
883
  elif task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_REACHED:
873
884
  df_sub_task = df_task[df_task["phase"] == TaskPhase.ACCEPTANCE.value]
885
+ elif task_completion_criteria == TaskCompletionCriteria.INSPECTION_REACHED:
886
+ df_sub_task = df_task[df_task["phase"].isin([TaskPhase.INSPECTION.value, TaskPhase.ACCEPTANCE.value])]
874
887
  else:
875
888
  assert_noreturn(task_completion_criteria)
876
889
 
@@ -998,12 +1011,16 @@ class WholeProductivityPerFirstAnnotationStartedDate:
998
1011
  str_task = "受入フェーズ完了状態"
999
1012
  elif self.task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_REACHED:
1000
1013
  str_task = "受入フェーズ"
1014
+ elif self.task_completion_criteria == TaskCompletionCriteria.INSPECTION_REACHED:
1015
+ str_task = "検査フェーズまたは受入フェーズ"
1001
1016
  else:
1002
1017
  assert_noreturn(self.task_completion_criteria)
1003
1018
 
1004
1019
  elm_list = ["<h4>注意</h4>", f"<p>「X日のタスク数」は、X日に教師付フェーズを開始したタスクの内、{str_task}であるタスクの個数です。</p>"]
1005
1020
  if self.task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_REACHED:
1006
1021
  elm_list.append("<p>タスクが受入フェーズに到達したら作業が完了したとみなしているため、受入作業時間は0にしています。</p>")
1022
+ elif self.task_completion_criteria == TaskCompletionCriteria.INSPECTION_REACHED:
1023
+ elm_list.append("<p>タスクが検査フェーズまたは受入フェーズに到達したら作業が完了したとみなしているため、検査作業時間と受入作業時間は0にしています。</p>")
1007
1024
 
1008
1025
  return Div(text=" ".join(elm_list))
1009
1026
 
@@ -14,7 +14,6 @@ import annofabapi
14
14
  import bokeh
15
15
  import bokeh.layouts
16
16
  import bokeh.palettes
17
- import numpy
18
17
  import pandas
19
18
  from bokeh.models.ui import UIElement
20
19
  from bokeh.plotting import ColumnDataSource
@@ -113,6 +112,20 @@ class WorktimePerDate:
113
112
  df["monitored_worktime_hour"] = df["monitored_annotation_worktime_hour"] + df["monitored_inspection_worktime_hour"] + df["monitored_acceptance_worktime_hour"]
114
113
  return WorktimePerDate(df)
115
114
 
115
+ def to_non_inspection_acceptance(self) -> WorktimePerDate:
116
+ """
117
+ 検査フェーズと受入フェーズの作業時間を0にした新しいインスタンスを生成します。
118
+
119
+ `--task_completion_criteria inspection_reached`を指定したときに利用します。
120
+ この場合、検査フェーズと受入フェーズの作業時間を無視して集計する必要があるためです。
121
+
122
+ """
123
+ df = self.df.copy()
124
+ df["monitored_inspection_worktime_hour"] = 0
125
+ df["monitored_acceptance_worktime_hour"] = 0
126
+ df["monitored_worktime_hour"] = df["monitored_annotation_worktime_hour"]
127
+ return WorktimePerDate(df)
128
+
116
129
  def is_empty(self) -> bool:
117
130
  """
118
131
  空のデータフレームを持つかどうかを返します。
@@ -442,13 +455,6 @@ class WorktimePerDate:
442
455
  df_cumulative = self._get_cumulative_dataframe(df_continuous_date)
443
456
  df_cumulative["dt_date"] = df_cumulative["date"].map(lambda e: datetime.datetime.fromisoformat(e).date())
444
457
 
445
- # bokeh3.0.3では、string型の列を持つpandas.DataFrameを描画できないため、改めてobject型に戻す
446
- # またpandas.NAを持つDataFrameも描画できないので、numpy.nanに変換する
447
- # TODO この問題が解決されたら、削除する
448
- # https://qiita.com/yuji38kwmt/items/b5da6ed521e827620186
449
- df_cumulative = df_cumulative.astype({"date": "object", "account_id": "object", "user_id": "object", "username": "object", "biography": "object"})
450
- df_cumulative.replace(pandas.NA, numpy.nan, inplace=True)
451
-
452
458
  line_count = 0
453
459
  plotted_users: list[tuple[str, str]] = []
454
460
  for user_index, user_id in enumerate(user_id_list):
@@ -10,6 +10,7 @@ from annofabapi.models import Task
10
10
 
11
11
  from annofabcli.common.facade import TaskQuery, match_task_with_query
12
12
  from annofabcli.common.utils import isoduration_to_hour
13
+ from annofabcli.statistics.visualization.model import TaskCompletionCriteria
13
14
 
14
15
  logger = logging.getLogger(__name__)
15
16
 
@@ -72,7 +73,7 @@ def filter_task_histories(task_histories: dict[str, list[dict[str, Any]]], *, st
72
73
  return {task_id: sub_task_history_list for task_id, sub_task_history_list in task_histories.items() if pred(sub_task_history_list)}
73
74
 
74
75
 
75
- def filter_tasks(tasks: list[dict[str, Any]], query: FilteringQuery, *, task_histories: dict[str, list[dict[str, Any]]]) -> list[Task]:
76
+ def filter_tasks(tasks: list[dict[str, Any]], task_completion_criteria: TaskCompletionCriteria, query: FilteringQuery, task_histories: dict[str, list[dict[str, Any]]]) -> list[Task]:
76
77
  """
77
78
  タスク一覧を絞り込みます。
78
79
 
@@ -95,6 +96,9 @@ def filter_tasks(tasks: list[dict[str, Any]], query: FilteringQuery, *, task_his
95
96
  """
96
97
 
97
98
  flag = True
99
+
100
+ flag = flag and task_completion_criteria.is_task_completed(arg_task)
101
+
98
102
  if query.task_query is not None:
99
103
  flag = flag and match_task_with_query(DcTask.from_dict(arg_task), query.task_query)
100
104
 
@@ -1,5 +1,11 @@
1
1
  from dataclasses import dataclass
2
2
  from enum import Enum
3
+ from typing import Any
4
+
5
+ from annofabapi.pydantic_models.task_phase import TaskPhase
6
+ from annofabapi.pydantic_models.task_status import TaskStatus
7
+
8
+ from annofabcli.common.type_util import assert_noreturn
3
9
 
4
10
 
5
11
  class WorktimeColumn(Enum):
@@ -32,3 +38,29 @@ class TaskCompletionCriteria(Enum):
32
38
  """タスクが受入フェーズの完了状態であれば「タスクの完了」とみなす"""
33
39
  ACCEPTANCE_REACHED = "acceptance_reached"
34
40
  """タスクが受入フェーズに到達したら「タスクの完了」とみなす"""
41
+ INSPECTION_REACHED = "inspection_reached"
42
+ """タスクが検査フェーズに到達したら「タスクの完了」とみなす"""
43
+
44
+ def is_task_completed(self, task: dict[str, Any]) -> bool:
45
+ """指定したタスクが、タスクの完了条件に合致するかどうかを判定します。
46
+
47
+ Args:
48
+ task: タスク情報。以下のキーを参照します。
49
+ * phase
50
+ * status
51
+
52
+ Returns:
53
+ タスクの完了条件に合致する場合はTrue、そうでない場合はFalse
54
+ """
55
+ if self == TaskCompletionCriteria.ACCEPTANCE_COMPLETED:
56
+ return task["phase"] == TaskPhase.ACCEPTANCE.value and task["status"] == TaskStatus.COMPLETE.value
57
+
58
+ elif self == TaskCompletionCriteria.ACCEPTANCE_REACHED:
59
+ return task["phase"] == TaskPhase.ACCEPTANCE.value
60
+
61
+ elif self == TaskCompletionCriteria.INSPECTION_REACHED:
62
+ # 受入フェーズも含む理由:検査フェーズに到達したタスクを「完了」とみなすならば、検査フェーズより後段フェーズである受入フェーズも「完了」とみなせるため
63
+ return task["phase"] in {TaskPhase.INSPECTION.value, TaskPhase.ACCEPTANCE.value}
64
+
65
+ else:
66
+ assert_noreturn(self)
@@ -37,6 +37,12 @@ class ProjectDir(DataClassJsonMixin):
37
37
  Args:
38
38
  project_dir: ``annofabcli statistics visualize``コマンドによって出力されたプロジェクトディレクトリ
39
39
  metadata:
40
+
41
+ タスクの完了条件(task_completion_criteria)は以下のいずれかです。
42
+ - acceptance_completed: 受入フェーズの完了状態
43
+ - acceptance_reached: 受入フェーズに到達
44
+ - inspection_reached: 検査フェーズまたは受入フェーズに到達
45
+ - annotation_started: 教師付フェーズを着手
40
46
  """
41
47
 
42
48
  FILENAME_WHOLE_PERFORMANCE = "全体の生産性と品質.csv"
@@ -454,7 +460,7 @@ class ProjectInfo(DataClassJsonMixin):
454
460
  measurement_datetime: str
455
461
  """計測日時。(2004-04-01T12:00+09:00形式)"""
456
462
  task_completion_criteria: TaskCompletionCriteria
457
- """タスクの完了条件"""
463
+ """タスクの完了条件(acceptance_completed / acceptance_reached / inspection_reached / annotation_started)"""
458
464
  query: FilteringQuery
459
465
  """集計対象を絞り込むためのクエリ"""
460
466
 
@@ -12,7 +12,7 @@ from typing import Any, Callable, Optional
12
12
 
13
13
  import annofabapi
14
14
  import pandas
15
- from annofabapi.models import ProjectMemberRole, TaskPhase, TaskStatus
15
+ from annofabapi.models import ProjectMemberRole, TaskPhase
16
16
 
17
17
  import annofabcli
18
18
  from annofabcli.common.cli import (
@@ -24,7 +24,6 @@ from annofabcli.common.cli import (
24
24
  get_list_from_args,
25
25
  )
26
26
  from annofabcli.common.facade import AnnofabApiFacade, TaskQuery
27
- from annofabcli.common.type_util import assert_noreturn
28
27
  from annofabcli.statistics.visualization.dataframe.actual_worktime import ActualWorktime
29
28
  from annofabcli.statistics.visualization.dataframe.annotation_count import AnnotationCount
30
29
  from annofabcli.statistics.visualization.dataframe.cumulative_productivity import (
@@ -123,7 +122,7 @@ class WriteCsvGraph:
123
122
 
124
123
  tasks = self.visualize_source_files.read_tasks_json()
125
124
  task_histories = self.visualize_source_files.read_task_histories_json()
126
- new_tasks = filter_tasks(tasks, self.filtering_query, task_histories=task_histories)
125
+ new_tasks = filter_tasks(tasks, self.task_completion_criteria, self.filtering_query, task_histories=task_histories)
127
126
  logger.debug(f"project_id='{self.project_id}' :: 集計対象タスクは {len(new_tasks)} / {len(tasks)} 件です。")
128
127
 
129
128
  self.task = Task.from_api_content(
@@ -534,14 +533,9 @@ class VisualizeStatistics(CommandLine):
534
533
  sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
535
534
 
536
535
  task_completion_criteria = TaskCompletionCriteria(args.task_completion_criteria)
537
- if task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_COMPLETED:
538
- task_query = TaskQuery(phase=TaskPhase.ACCEPTANCE, status=TaskStatus.COMPLETE)
539
- elif task_completion_criteria == TaskCompletionCriteria.ACCEPTANCE_REACHED:
540
- task_query = TaskQuery(phase=TaskPhase.ACCEPTANCE)
541
- else:
542
- assert_noreturn(task_completion_criteria)
543
536
 
544
537
  dict_task_query = annofabcli.common.cli.get_json_from_args(args.task_query)
538
+ task_query: Optional[TaskQuery] = None
545
539
  if dict_task_query is not None:
546
540
  task_query = TaskQuery.from_dict(dict_task_query)
547
541
  logger.warning("引数 '--task_query' は非推奨です。代わりに '--task_completion_criteria' を指定してください。")
@@ -106,7 +106,7 @@ def plot_video_duration(
106
106
  bins_sequence = numpy.arange(0, max_duration + bin_width, bin_width)
107
107
 
108
108
  if bins_sequence[-1] == max_duration:
109
- bins_sequence = numpy.append(bins_sequence, bins_sequence[-1] + bin_width) # type: ignore[assignment]
109
+ bins_sequence = numpy.append(bins_sequence, bins_sequence[-1] + bin_width)
110
110
 
111
111
  bins: Union[int, numpy.ndarray] = bins_sequence
112
112
  else:
@@ -148,12 +148,33 @@ def get_first_acceptance_reached_datetime(task_histories: list[TaskHistory]) ->
148
148
  if history["phase"] != TaskPhase.ACCEPTANCE.value:
149
149
  continue
150
150
 
151
+ assert index > 0
151
152
  first_acceptance_reached_datetime = task_histories[index - 1]["ended_datetime"]
152
153
  assert first_acceptance_reached_datetime is not None
153
154
  return first_acceptance_reached_datetime
154
155
  return None
155
156
 
156
157
 
158
+ def get_first_inspection_reached_datetime(task_histories: list[TaskHistory]) -> Optional[str]:
159
+ """
160
+ はじめて検査フェーズに到達した日時を取得する。
161
+ 検査フェーズを着手した日時とは異なる。
162
+ 必ず`first_inspection_started_datetime`よりも前の日時になる。
163
+
164
+ たとえば教師付フェーズで提出して検査フェーズに到達した場合、教師付フェーズを提出した日時が「検査フェーズに到達した日時」になる。
165
+
166
+ """
167
+ for index, history in enumerate(task_histories):
168
+ if history["phase"] != TaskPhase.INSPECTION.value:
169
+ continue
170
+
171
+ assert index > 0
172
+ first_inspection_reached_datetime = task_histories[index - 1]["ended_datetime"]
173
+ assert first_inspection_reached_datetime is not None
174
+ return first_inspection_reached_datetime
175
+ return None
176
+
177
+
157
178
  def is_acceptance_phase_skipped(task_histories: list[TaskHistory]) -> bool:
158
179
  """抜取受入によって、受入フェーズでスキップされたことがあるかを取得する。
159
180
 
@@ -332,7 +353,8 @@ class AddingAdditionalInfoToTask:
332
353
  first_task_history = get_first_task_history(task_histories, phase)
333
354
  self._add_task_history_info(task, first_task_history, column_prefix=f"first_{phase.value}")
334
355
 
335
- # 初めて受入が完了した日時
356
+ # 初めて~になった日時
357
+ task["first_inspection_reached_datetime"] = get_first_inspection_reached_datetime(task_histories)
336
358
  task["first_acceptance_reached_datetime"] = get_first_acceptance_reached_datetime(task_histories)
337
359
  task["first_acceptance_completed_datetime"] = get_first_acceptance_completed_datetime(task_histories)
338
360
 
@@ -387,6 +409,7 @@ class TasksAddedTaskHistoryOutput:
387
409
  def _get_output_target_columns() -> list[str]:
388
410
  base_columns = [
389
411
  # タスクの基本情報
412
+ "project_id",
390
413
  "task_id",
391
414
  "phase",
392
415
  "phase_stage",
@@ -409,6 +432,7 @@ class TasksAddedTaskHistoryOutput:
409
432
  # 差し戻し回数
410
433
  "number_of_rejections_by_inspection",
411
434
  "number_of_rejections_by_acceptance",
435
+ "first_inspection_reached_datetime",
412
436
  "first_acceptance_reached_datetime",
413
437
  "first_acceptance_completed_datetime",
414
438
  "completed_datetime",
@@ -432,17 +456,14 @@ class TasksAddedTaskHistoryOutput:
432
456
 
433
457
  def output(self, output_path: Path, output_format: FormatArgument) -> None:
434
458
  task_list = self.task_list
435
- if len(task_list) == 0:
436
- logger.info("タスク一覧の件数が0件のため、出力しません。")
437
- return
438
-
439
- logger.debug(f"タスク一覧の件数: {len(task_list)}")
459
+ logger.debug(f"タスク {len(task_list)} 件の情報を出力します。")
440
460
  if output_format == FormatArgument.CSV:
441
- df_task = pandas.DataFrame(task_list)
461
+ df_task = pandas.DataFrame(task_list, columns=self._get_output_target_columns())
442
462
  print_csv(
443
463
  df_task[self._get_output_target_columns()],
444
464
  output=output_path,
445
465
  )
466
+
446
467
  elif output_format == FormatArgument.JSON:
447
468
  print_json(task_list, is_pretty=False, output=output_path)
448
469
  elif output_format == FormatArgument.PRETTY_JSON:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: annofabcli
3
- Version: 1.105.1
3
+ Version: 1.106.1
4
4
  Summary: Utility Command Line Interface for AnnoFab
5
5
  Author: Kurusugawa Computer Inc.
6
6
  License: MIT
@@ -1,15 +1,15 @@
1
1
  annofabcli/__init__.py,sha256=fdBtxy5rOI8zi26jf0hmXS5KTBjQIsm2b9ZUSAIR558,319
2
- annofabcli/__main__.py,sha256=83jLGTlNGoXaYU3fz9akYXoL3kgfIuaVXo_TkpK72IU,5267
2
+ 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
6
  annofabcli/annotation/change_annotation_attributes.py,sha256=Zjqax-sb7ujJnTUVv4io0GFmZzfqx0rpN2N1W86uVTs,17092
7
- annofabcli/annotation/change_annotation_attributes_per_annotation.py,sha256=4OkFOasRpW86akmgZOdKwnt__b-Q6uCwILInxgiy5o8,13878
7
+ annofabcli/annotation/change_annotation_attributes_per_annotation.py,sha256=iU199MlMfD-nfyRiHL44Zqrvr12vOeUHXyxCrDgaivI,14130
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
- annofabcli/annotation/dump_annotation.py,sha256=CJ10zJUm9C1M1VhB_ZtdkWPncrZTx9h9JyxmLn7f3lo,8397
12
+ annofabcli/annotation/dump_annotation.py,sha256=Q-p6f5XBs7khDgrfY5Q3CGLBMKEerJWO_CQ8_73UXVM,9972
13
13
  annofabcli/annotation/import_annotation.py,sha256=39w-LwhNrT4-20NR5Hy-cn_cUFVVWVnMp9x1d8aITn0,33369
14
14
  annofabcli/annotation/list_annotation.py,sha256=uKcOuGC7lzd6vVbzizkiZtYdXJ7EzY0iifuiqKl2wQM,10707
15
15
  annofabcli/annotation/list_annotation_count.py,sha256=T9fbaoxWeDJIVgW_YgHRldbwrVZWiE-57lfJrDQrj80,6474
@@ -48,14 +48,14 @@ annofabcli/comment/subcommand_comment.py,sha256=gd8w8ArXM1Tq9VUduDgn8m4G6HwevRWJ
48
48
  annofabcli/comment/utils.py,sha256=aUj7U6MtGh64F3Ko83y4NKPKyWAqcg-c1XLqjkmIpSk,350
49
49
  annofabcli/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
50
  annofabcli/common/bokeh.py,sha256=x5-Siok3cIhq-biTOb3JG91_q--ob1m48TDgB72P1Hc,1071
51
- annofabcli/common/cli.py,sha256=zGbelWfEct1UXVDp8l2NM2lmYa8B2bRFAt8o5O4W1gY,22381
51
+ annofabcli/common/cli.py,sha256=pmbieBx1-Eb-mlVqJAG0dJnalggczdbBly_u-qzHX84,21666
52
52
  annofabcli/common/dataclasses.py,sha256=FRDihsB-H5P5ioVE3kfJtorsS5EbSMySlHrp8BJ3ktg,385
53
53
  annofabcli/common/download.py,sha256=b3e5tVn1dmM1BMti67WwGzDCRH7hYJQEgk0xxkkuMIw,14003
54
54
  annofabcli/common/enums.py,sha256=pnMZEk8ADK2qO2Hmujx6NxeCwvSAEDNhmgK4ajPSC9Q,1233
55
55
  annofabcli/common/exceptions.py,sha256=trgC5eqvy7jgqOQ41pbAOC__uxy19GgrM9NAgkH_lBw,2051
56
56
  annofabcli/common/facade.py,sha256=1d5DxC4O5yHr0gisqaqgkacY5Omn1X-L1syPnYPyiwQ,19401
57
57
  annofabcli/common/image.py,sha256=qUsQXEYmttT443BOiaxLthccE7hLji5toLjgSdneNWA,16706
58
- annofabcli/common/pandas.py,sha256=IW9xqHkdRF1I6YZc7CP_9tkGxJuu1MKEXFILjhaNUU0,449
58
+ annofabcli/common/pandas.py,sha256=Vyozu52UiZ0aCVtQ9jySxT_LrLMwpQvvpM8Uu3S2nCs,476
59
59
  annofabcli/common/type_util.py,sha256=i3r5pFtRYQwJrYkl1-lVQi8XOePQxTUX_cAHgBTsagM,224
60
60
  annofabcli/common/typing.py,sha256=_AcEogoaMbib0esfN2RvHshAZH0oyRb2Ro9-rbn7NJ8,330
61
61
  annofabcli/common/utils.py,sha256=Eb4DS5j_EVdZW_YnsvIgjTbgWcjHd560plck2_WgrCI,9545
@@ -114,7 +114,7 @@ annofabcli/organization_member/subcommand_organization_member.py,sha256=iK-Y4PxG
114
114
  annofabcli/project/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
115
115
  annofabcli/project/change_organization_of_project.py,sha256=yeR8Xu4nmvrA4jGfsPDmFxf-qJxvEVQAvluierJRMxc,11300
116
116
  annofabcli/project/change_project_status.py,sha256=AQQFFYjUnwrFshuYS_xSGyLAp2lJ-7SSIuaigPTLBYk,8657
117
- annofabcli/project/copy_project.py,sha256=9UD2ElqodOyOwlvT-yqBVozA_9FivBWGLcEckG9B7m0,6972
117
+ annofabcli/project/copy_project.py,sha256=O1TooJ0VVbRP84d4Y-8e6Z7pKMwhBEwPu3EBv15hPlY,7131
118
118
  annofabcli/project/diff_projects.py,sha256=OsR0otV6MOCbUgGEDsYganu5cuHYZbpHbBAqujwBAw8,16531
119
119
  annofabcli/project/list_project.py,sha256=OWnbCyShI4ceugxML8kSAD2nkyld5MlpXfbvQu2__4Y,10367
120
120
  annofabcli/project/put_project.py,sha256=dsRJfx2ga4mmYci40q91UzXeQN6m_3fMU9mrcFRpY9Q,6033
@@ -137,11 +137,11 @@ annofabcli/stat_visualization/write_performance_rating_csv.py,sha256=0j78z5uRULo
137
137
  annofabcli/statistics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
138
138
  annofabcli/statistics/histogram.py,sha256=CvzDxT2cKLSnBGSqkZE6p92PayGxYYja1YyB24M4ALU,3245
139
139
  annofabcli/statistics/linegraph.py,sha256=0kr7jVBNMiM2ECYhv3Ry5RitElKerSl9ZKxbKzfiplI,12494
140
- annofabcli/statistics/list_annotation_area.py,sha256=nGqhqPEHLPUccnLCmA9nmwvmYbZYlh5ZxiHNszZ1ei8,12329
141
- annofabcli/statistics/list_annotation_attribute.py,sha256=L_wmcUGS0Lu3tnRmidDjYK3bCNdvwDjAH64r5__MQ_w,12522
142
- annofabcli/statistics/list_annotation_attribute_filled_count.py,sha256=g-MihJwEoA1ZnLTJOGX-dn6hI5aUdqQvh2KWbREbQ9s,29033
143
- annofabcli/statistics/list_annotation_count.py,sha256=cBmUTYifB7UESEFets7DMDn8_GKDBBDJ2vt1q_56fcc,52747
144
- annofabcli/statistics/list_annotation_duration.py,sha256=ps0UT50W_6ZL5_cZNsSo7Dp-cSx77m1IF4QMBkwRt98,31875
140
+ annofabcli/statistics/list_annotation_area.py,sha256=RC6Pkhh_ps8fMZuDSQqhk2RdMpJ_z5TA9ax42tuJRFM,12567
141
+ annofabcli/statistics/list_annotation_attribute.py,sha256=lmvT3b1mVX4PRGWrBUfZafCn3c_hb_hx3WKXnHGcyyk,12754
142
+ annofabcli/statistics/list_annotation_attribute_filled_count.py,sha256=UF8wB-kfUnHCIPykb2Z2slyEcp9aHrzcbFbOs_3Z8-A,29341
143
+ annofabcli/statistics/list_annotation_count.py,sha256=4uTRR63Db-WDGRQt3AC8mPaRfFgbG5d27A036GNG3D0,53151
144
+ annofabcli/statistics/list_annotation_duration.py,sha256=WxHfzF4tYkveIdkaWoJZ6x7ddBMWkfz1YM_vxMub3jY,32165
145
145
  annofabcli/statistics/list_video_duration.py,sha256=OCDXPOP2z7flE8NBaQD1lDkU9JG5mn3PfACijoR-5s0,9163
146
146
  annofabcli/statistics/list_worktime.py,sha256=nr--GtFY-oyFuu8M0EsUqcVxX26gjeP09LYUcdeptyk,6456
147
147
  annofabcli/statistics/scatter.py,sha256=C3hTlm_QfGBiY4KjZ-8D_u_Rk53a9f4jszx4iNZgp9w,10945
@@ -151,30 +151,30 @@ annofabcli/statistics/summarize_task_count_by_task_id_group.py,sha256=eRPl89Oh7_
151
151
  annofabcli/statistics/summarize_task_count_by_user.py,sha256=rwhQwO6g2No9AqLEAbAfnlg_6CkgB3CRkZiaNnXCL0I,6857
152
152
  annofabcli/statistics/visualize_annotation_count.py,sha256=7OATuGa2okq80unuTe-X30CBVkrlMLDN5Y-Q_5mB6eI,22138
153
153
  annofabcli/statistics/visualize_annotation_duration.py,sha256=9JH9MirhOyCmjcChFJMtfnFIV2k4sucP9PAwNKMcbtE,21022
154
- annofabcli/statistics/visualize_statistics.py,sha256=Hey3LqTFoXT0zKP_KMgszaVC8xIZPpep5sxpuefWKG4,38764
155
- annofabcli/statistics/visualize_video_duration.py,sha256=PYiVT7CDm3g5WLNRF20s7xGluJctO1mtC6y3wNu-eK4,16651
154
+ annofabcli/statistics/visualize_statistics.py,sha256=YiT-KCCFQNF-v56hA8WjX37b_CGjMk2Ei-NbW7aHG-k,38384
155
+ annofabcli/statistics/visualize_video_duration.py,sha256=yY18H0-boNy4-MQWUM_xBTMzxDUQ51TrVtz8mbsB_dI,16623
156
156
  annofabcli/statistics/visualization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
157
- annofabcli/statistics/visualization/filtering_query.py,sha256=6QH0hfyg2VPeSjiJrd3ECMLbZCTZfQMH-K2fdn4rlaE,4152
158
- annofabcli/statistics/visualization/model.py,sha256=5LSp0u63nqeILUl3XdYEhqqi_hq2rFbzyKHYlXZo2S0,896
159
- annofabcli/statistics/visualization/project_dir.py,sha256=khcSS0R6X1sm1Oq8Tr7HpHPNFaHbh3jSPxaaPn9GuyA,23839
157
+ annofabcli/statistics/visualization/filtering_query.py,sha256=kqGOa1YdQ62kTLlkiIHFiYNju4Pg9rXpsC4Cph-QjLo,4355
158
+ annofabcli/statistics/visualization/model.py,sha256=sKk4gC7qkt6etCnbpCNZpJEhFJf5Vf7eVOG53gyfWvk,2426
159
+ annofabcli/statistics/visualization/project_dir.py,sha256=-60n29ySvgsfRTbFgTOYr6E7LHJezVQm3FON88LQMKU,24291
160
160
  annofabcli/statistics/visualization/visualization_source_files.py,sha256=SFY7WXUtjECB8l7zP-exawocrTiZ0UI7Z5sjgq4J_g4,8641
161
161
  annofabcli/statistics/visualization/dataframe/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
162
162
  annofabcli/statistics/visualization/dataframe/actual_worktime.py,sha256=7nsHlvN5cDzXIw-u_MSAZf4nlSSY56IlunSmnODXTbY,1916
163
163
  annofabcli/statistics/visualization/dataframe/annotation_count.py,sha256=gv9iFpSLu_cB63G3Z9dGImT9jr_Q6U_mrmG8VXsxJnk,3940
164
- annofabcli/statistics/visualization/dataframe/cumulative_productivity.py,sha256=DyaskG9fT3K2rWR04z4QIYgdrAjR_O3YYDmsVT23NQg,16144
164
+ annofabcli/statistics/visualization/dataframe/cumulative_productivity.py,sha256=Z9gxGCfgQra0M0LAq3bUhGxXnwRqZ8Gt-zIo2DPs6Nc,15800
165
165
  annofabcli/statistics/visualization/dataframe/custom_production_volume.py,sha256=5ELLiQJ5sNKdVKmYYVeZW4nedDg1CVGxMDdF5TUUX5c,2142
166
166
  annofabcli/statistics/visualization/dataframe/input_data_count.py,sha256=wDRFtoIWw_Gy2bPZ7LBx3eMO3LdUdjbQKS9mncXav6I,1654
167
167
  annofabcli/statistics/visualization/dataframe/inspection_comment_count.py,sha256=RxpQzRy4U2hKEpgbksUXotcxH2sKz__NO20mxpMqK1w,4382
168
168
  annofabcli/statistics/visualization/dataframe/productivity_per_date.py,sha256=_AibOSwyeGM6KeYqknN40M8vS4Tl6l45tC0ua1AHFFc,27087
169
169
  annofabcli/statistics/visualization/dataframe/project_performance.py,sha256=q480Lz1LwQD6RvyIOq-ZsXqeeKabsVpHRoM0nD_DaGI,8482
170
- annofabcli/statistics/visualization/dataframe/task.py,sha256=HBquPn6K17WPgb3nyeMS44M57srD5LpUm8wtOXJLkTU,24098
170
+ annofabcli/statistics/visualization/dataframe/task.py,sha256=v5vUbKJAuqqcKeumrAc4t2PKEX7jt86xFqqko0LrI98,25044
171
171
  annofabcli/statistics/visualization/dataframe/task_history.py,sha256=FZ3MLqOiM6834fxWZKP0Hy9WDRSSLCYm6kNzdWW7-S8,2744
172
- annofabcli/statistics/visualization/dataframe/task_worktime_by_phase_user.py,sha256=TKdZmjr-jvnxOxsSnSCbloV9EQ3RB3nKv1tfIve2EZE,12974
172
+ annofabcli/statistics/visualization/dataframe/task_worktime_by_phase_user.py,sha256=khXqxQ-v7Q1PBtttQmPCCbVLraeqNCPi2tkhTxxaZPg,13641
173
173
  annofabcli/statistics/visualization/dataframe/user.py,sha256=EHn7nlf6D6UX-gsVXy8m_3QaCsHsUhr0iy2rbNozOgc,1707
174
- annofabcli/statistics/visualization/dataframe/user_performance.py,sha256=2WX_D3I9_mn0mIPfsb2XCJINs31TmID4F9SNfIPccsM,56093
174
+ annofabcli/statistics/visualization/dataframe/user_performance.py,sha256=X0jXV0wY8AZC8hoV62sJd9GRThqTf5RsaVciLeCvGlk,56628
175
175
  annofabcli/statistics/visualization/dataframe/whole_performance.py,sha256=kw5Cww0fwDfwriTtxiT0l3wr7YAMA9eGjvp84G9MN9I,12449
176
- annofabcli/statistics/visualization/dataframe/whole_productivity_per_date.py,sha256=q3I00aCztXlMX3JW4XLe7apNRS4_gQVhOynlsaYBZVY,51635
177
- annofabcli/statistics/visualization/dataframe/worktime_per_date.py,sha256=9_ZVtXlNDFUKYWwcb_Pd_1IE5JUpTGmZdAPAS0nl1Lo,21235
176
+ annofabcli/statistics/visualization/dataframe/whole_productivity_per_date.py,sha256=F6KhXvzrUx61E0jqOiU2IGb6ycLH82T80JyzYVpSaC8,53683
177
+ annofabcli/statistics/visualization/dataframe/worktime_per_date.py,sha256=wt0paPy2cVVzLUJhbV12-6bInikbQoxRFnOHY7BcI9o,21342
178
178
  annofabcli/supplementary/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
179
179
  annofabcli/supplementary/delete_supplementary_data.py,sha256=dA4n61SSkTbDKDqjVldHIEAxbPQXyrQciwI4RDuC35U,13537
180
180
  annofabcli/supplementary/list_supplementary_data.py,sha256=Rh9aOa3PwukVxnnbXTtMG0qAASc3JkaqRt7KrMGZQRk,7635
@@ -193,7 +193,7 @@ annofabcli/task/download_task_json.py,sha256=Ocjecmdf2WV_Sq3u1InfMLIsT3XSw0ojyJm
193
193
  annofabcli/task/list_all_tasks.py,sha256=FpFzosCZs2EZNSQMHzWse4z_zjZeiVF5BFmbwb97UYg,6368
194
194
  annofabcli/task/list_all_tasks_added_task_history.py,sha256=fkdiuo64iS7xxvIfGKzSiUPPEMiCVnJjjcAtMxe2Ngs,9551
195
195
  annofabcli/task/list_tasks.py,sha256=cmfqbP2Sr1dlpvpJLJscKq31BUdXVaajr_agiecKWXg,10051
196
- annofabcli/task/list_tasks_added_task_history.py,sha256=c8kxh9dssOZw6WK3Zpet1lNYr311xiiPJ1JGv97u1Xs,21470
196
+ annofabcli/task/list_tasks_added_task_history.py,sha256=JI3Rxn9MwXN07Cu2wDfaIXAzm3MlNm1kIh1SZHbxZr4,22488
197
197
  annofabcli/task/put_tasks.py,sha256=HADm-sP8zs523MlDThlwrGIVIEtR4Md2KlpJbWhCCTk,13745
198
198
  annofabcli/task/put_tasks_by_count.py,sha256=kIjvWtgZ53Smcm8N-raZedQCubx1TWoQT-BYyioYDrA,5999
199
199
  annofabcli/task/reject_tasks.py,sha256=2mRxIsP63N2xcVXQ0xeFqF0VkuUQP6C_bsFEqG-qNf0,22027
@@ -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.105.1.dist-info/METADATA,sha256=1wpj99TzN1VpD56ObB9viB1goGsbs7qUMSUhQAk3DHY,5286
213
- annofabcli-1.105.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
214
- annofabcli-1.105.1.dist-info/entry_points.txt,sha256=C2uSUc-kkLJpoK_mDL5FEMAdorLEMPfwSf8VBMYnIFM,56
215
- annofabcli-1.105.1.dist-info/licenses/LICENSE,sha256=pcqWYfxFtxBzhvKp3x9MXNM4xciGb2eFewaRhXUNHlo,1081
216
- annofabcli-1.105.1.dist-info/RECORD,,
212
+ annofabcli-1.106.1.dist-info/METADATA,sha256=Yn9OyrfluKXXvCiFDNavFc3UnA3wnd6i1gYTRI43V0M,5286
213
+ annofabcli-1.106.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
214
+ annofabcli-1.106.1.dist-info/entry_points.txt,sha256=C2uSUc-kkLJpoK_mDL5FEMAdorLEMPfwSf8VBMYnIFM,56
215
+ annofabcli-1.106.1.dist-info/licenses/LICENSE,sha256=pcqWYfxFtxBzhvKp3x9MXNM4xciGb2eFewaRhXUNHlo,1081
216
+ annofabcli-1.106.1.dist-info/RECORD,,