annofabcli 1.114.4__py3-none-any.whl → 1.114.6__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.
@@ -7,7 +7,6 @@ import collections
7
7
  import copy
8
8
  import json
9
9
  import logging
10
- import sys
11
10
  import tempfile
12
11
  import zipfile
13
12
  from collections import Counter, defaultdict
@@ -34,7 +33,6 @@ from dataclasses_json import DataClassJsonMixin, config
34
33
 
35
34
  import annofabcli.common.cli
36
35
  from annofabcli.common.cli import (
37
- COMMAND_LINE_ERROR_STATUS_CODE,
38
36
  ArgumentParser,
39
37
  CommandLine,
40
38
  build_annofabapi_resource_and_login,
@@ -103,6 +101,15 @@ def encode_annotation_count_by_attribute(
103
101
 
104
102
  @dataclass(frozen=True)
105
103
  class AnnotationCounter(abc.ABC):
104
+ """
105
+ アノテーションの集計情報を保持する抽象基底クラス。
106
+
107
+ Attributes:
108
+ annotation_count: アノテーションの総数
109
+ annotation_count_by_label: ラベルごとのアノテーション数
110
+ annotation_count_by_attribute: 属性値ごとのアノテーション数
111
+ """
112
+
106
113
  annotation_count: int
107
114
  annotation_count_by_label: Counter[str]
108
115
  annotation_count_by_attribute: Counter[AttributeValueKey] = field(
@@ -114,6 +121,18 @@ class AnnotationCounter(abc.ABC):
114
121
 
115
122
  @dataclass(frozen=True)
116
123
  class AnnotationCounterByTask(AnnotationCounter, DataClassJsonMixin):
124
+ """
125
+ タスク単位でのアノテーション集計情報。
126
+
127
+ Attributes:
128
+ project_id: プロジェクトID
129
+ task_id: タスクID
130
+ task_status: タスクのステータス
131
+ task_phase: タスクのフェーズ
132
+ task_phase_stage: タスクフェーズのステージ番号
133
+ input_data_count: タスクに含まれる入力データの数
134
+ """
135
+
117
136
  project_id: str
118
137
  task_id: str
119
138
  task_status: TaskStatus
@@ -124,6 +143,21 @@ class AnnotationCounterByTask(AnnotationCounter, DataClassJsonMixin):
124
143
 
125
144
  @dataclass(frozen=True)
126
145
  class AnnotationCounterByInputData(AnnotationCounter, DataClassJsonMixin):
146
+ """
147
+ 入力データ単位でのアノテーション集計情報。
148
+
149
+ Attributes:
150
+ project_id: プロジェクトID
151
+ task_id: タスクID
152
+ task_status: タスクのステータス
153
+ task_phase: タスクのフェーズ
154
+ task_phase_stage: タスクフェーズのステージ番号
155
+ input_data_id: 入力データID
156
+ input_data_name: 入力データ名
157
+ updated_datetime: アノテーションJSONに格納されているアノテーションの更新日時
158
+ frame_no: フレーム番号。アノテーションJSONには含まれていない情報なので、Optionalにする
159
+ """
160
+
127
161
  project_id: str
128
162
  task_id: str
129
163
  task_status: TaskStatus
@@ -139,6 +173,18 @@ class AnnotationCounterByInputData(AnnotationCounter, DataClassJsonMixin):
139
173
 
140
174
 
141
175
  def lazy_parse_simple_annotation_by_input_data(annotation_path: Path) -> Iterator[SimpleAnnotationParser]:
176
+ """
177
+ アノテーションzipまたはディレクトリから、入力データ単位でアノテーションを遅延パースするイテレータを返します。
178
+
179
+ Args:
180
+ annotation_path: アノテーションzipまたはzipを展開したディレクトリのパス
181
+
182
+ Returns:
183
+ SimpleAnnotationParserのイテレータ
184
+
185
+ Raises:
186
+ RuntimeError: 指定されたパスが存在しない場合、またはzipファイルでもディレクトリでもない場合
187
+ """
142
188
  if not annotation_path.exists():
143
189
  raise RuntimeError(f"'{annotation_path}' は存在しません。")
144
190
 
@@ -151,6 +197,18 @@ def lazy_parse_simple_annotation_by_input_data(annotation_path: Path) -> Iterato
151
197
 
152
198
 
153
199
  def lazy_parse_simple_annotation_by_task(annotation_path: Path) -> Iterator[SimpleAnnotationParserByTask]:
200
+ """
201
+ アノテーションzipまたはディレクトリから、タスク単位でアノテーションを遅延パースするイテレータを返します。
202
+
203
+ Args:
204
+ annotation_path: アノテーションzipまたはzipを展開したディレクトリのパス
205
+
206
+ Returns:
207
+ SimpleAnnotationParserByTaskのイテレータ
208
+
209
+ Raises:
210
+ RuntimeError: 指定されたパスが存在しない場合、またはzipファイルでもディレクトリでもない場合
211
+ """
154
212
  if not annotation_path.exists():
155
213
  raise RuntimeError(f"'{annotation_path}' は存在しません。")
156
214
 
@@ -166,12 +224,17 @@ class ListAnnotationCounterByInputData:
166
224
  """入力データ単位で、ラベルごと/属性ごとのアノテーション数を集計情報を取得するメソッドの集まり。
167
225
 
168
226
  Args:
169
- target_labels: 集計対象のラベル(label_name_en
170
- target_attribute_names: 集計対象の属性名
171
- non_target_labels: 集計対象外のラベル
172
- non_target_attribute_names: 集計対象外の属性名のキー。
227
+ target_labels: 集計対象のラベル(label_name_en)。`non_target_labels`との同時指定不可。
228
+ target_attribute_names: 集計対象の属性名。`non_target_attribute_names`との同時指定不可。
229
+ non_target_labels: 集計対象外のラベル。`target_labels`との同時指定不可。
230
+ non_target_attribute_names: 集計対象外の属性名のキー。`target_attribute_names`との同時指定不可。
231
+ target_attribute_names_only: 集計対象の属性名(属性名のみでフィルタリング)。ラベル名に関係なく、指定された属性名のみが対象になります。
173
232
  frame_no_map: key:task_id,input_data_idのtuple, value:フレーム番号
174
233
 
234
+ Raises:
235
+ ValueError: `target_labels`と`non_target_labels`の両方が指定された場合、
236
+ または`target_attribute_names`と`non_target_attribute_names`の両方が指定された場合に発生します。
237
+
175
238
  """
176
239
 
177
240
  def __init__(
@@ -181,12 +244,19 @@ class ListAnnotationCounterByInputData:
181
244
  non_target_labels: Collection[str] | None = None,
182
245
  target_attribute_names: Collection[AttributeNameKey] | None = None,
183
246
  non_target_attribute_names: Collection[AttributeNameKey] | None = None,
247
+ target_attribute_names_only: Collection[str] | None = None,
184
248
  frame_no_map: dict[tuple[str, str], int] | None = None,
185
249
  ) -> None:
250
+ if target_labels is not None and non_target_labels is not None:
251
+ raise ValueError("`target_labels`と`non_target_labels`の両方が指定されています。")
252
+ if target_attribute_names is not None and non_target_attribute_names is not None:
253
+ raise ValueError("`target_attribute_names`と`non_target_attribute_names`の両方が指定されています。")
254
+
186
255
  self.target_labels = set(target_labels) if target_labels is not None else None
187
256
  self.target_attribute_names = set(target_attribute_names) if target_attribute_names is not None else None
188
257
  self.non_target_labels = set(non_target_labels) if non_target_labels is not None else None
189
258
  self.non_target_attribute_names = set(non_target_attribute_names) if non_target_attribute_names is not None else None
259
+ self.target_attribute_names_only = set(target_attribute_names_only) if target_attribute_names_only is not None else None
190
260
  self.frame_no_map = frame_no_map
191
261
 
192
262
  def get_annotation_counter(
@@ -241,6 +311,14 @@ class ListAnnotationCounterByInputData:
241
311
  if (label, attribute_name) in self.target_attribute_names
242
312
  }
243
313
  )
314
+ if self.target_attribute_names_only is not None:
315
+ annotation_count_by_attribute = collections.Counter(
316
+ {
317
+ (label, attribute_name, attribute_value): count
318
+ for (label, attribute_name, attribute_value), count in annotation_count_by_attribute.items()
319
+ if attribute_name in self.target_attribute_names_only
320
+ }
321
+ )
244
322
  if self.non_target_attribute_names is not None:
245
323
  annotation_count_by_attribute = collections.Counter(
246
324
  {
@@ -309,7 +387,20 @@ class ListAnnotationCounterByInputData:
309
387
 
310
388
 
311
389
  class ListAnnotationCounterByTask:
312
- """タスク単位で、ラベルごと/属性ごとのアノテーション数を集計情報を取得するメソッドの集まり。"""
390
+ """タスク単位で、ラベルごと/属性ごとのアノテーション数を集計情報を取得するメソッドの集まり。
391
+
392
+ Args:
393
+ target_labels: 集計対象のラベル(label_name_en)。`non_target_labels`との同時指定不可。
394
+ target_attribute_names: 集計対象の属性名。`non_target_attribute_names`との同時指定不可。
395
+ non_target_labels: 集計対象外のラベル。`target_labels`との同時指定不可。
396
+ non_target_attribute_names: 集計対象外の属性名のキー。`target_attribute_names`との同時指定不可。
397
+ target_attribute_names_only: 集計対象の属性名(属性名のみでフィルタリング)。ラベル名に関係なく、指定された属性名のみが対象になります。
398
+
399
+ Raises:
400
+ ValueError: `target_labels`と`non_target_labels`の両方が指定された場合、
401
+ または`target_attribute_names`と`non_target_attribute_names`の両方が指定された場合に発生します。
402
+
403
+ """
313
404
 
314
405
  def __init__(
315
406
  self,
@@ -318,12 +409,19 @@ class ListAnnotationCounterByTask:
318
409
  non_target_labels: Collection[str] | None = None,
319
410
  target_attribute_names: Collection[AttributeNameKey] | None = None,
320
411
  non_target_attribute_names: Collection[AttributeNameKey] | None = None,
412
+ target_attribute_names_only: Collection[str] | None = None,
321
413
  ) -> None:
414
+ if target_labels is not None and non_target_labels is not None:
415
+ raise ValueError("`target_labels`と`non_target_labels`の両方が指定されています。")
416
+ if target_attribute_names is not None and non_target_attribute_names is not None:
417
+ raise ValueError("`target_attribute_names`と`non_target_attribute_names`の両方が指定されています。")
418
+
322
419
  self.counter_by_input_data = ListAnnotationCounterByInputData(
323
420
  target_labels=target_labels,
324
421
  non_target_labels=non_target_labels,
325
422
  target_attribute_names=target_attribute_names,
326
423
  non_target_attribute_names=non_target_attribute_names,
424
+ target_attribute_names_only=target_attribute_names_only,
327
425
  )
328
426
 
329
427
  def get_annotation_counter(self, task_parser: SimpleAnnotationParserByTask) -> AnnotationCounterByTask:
@@ -411,41 +509,31 @@ class ListAnnotationCounterByTask:
411
509
  class AttributeCountCsv:
412
510
  """
413
511
  属性値ごとのアノテーション数を記載するCSV。
414
-
415
- Args:
416
- selective_attribute_value_max_count: 選択肢系の属性の値の個数の上限。これを超えた場合は、非選択肢系属性(トラッキングIDやアノテーションリンクなど)とみなす
417
-
418
512
  """
419
513
 
420
- def __init__(self, selective_attribute_value_max_count: int = 20) -> None:
421
- self.selective_attribute_value_max_count = selective_attribute_value_max_count
422
-
423
- def _only_selective_attribute(self, columns: list[AttributeValueKey]) -> list[AttributeValueKey]:
424
- """
425
- 選択肢系の属性に対応する列のみ抽出する。
426
- 属性値の個数が多い場合、非選択肢系の属性(トラッキングIDやアノテーションリンクなど)の可能性があるため、それらを除外する。
427
- CSVの列数を増やしすぎないための対策。
514
+ def _value_columns(
515
+ self,
516
+ counter_list: Collection[AnnotationCounter],
517
+ prior_attribute_columns: list[AttributeValueKey] | None,
518
+ ) -> list[AttributeValueKey]:
428
519
  """
429
- attribute_name_list: list[AttributeNameKey] = []
430
- for label, attribute_name, _ in columns:
431
- attribute_name_list.append((label, attribute_name))
520
+ CSV出力のための属性値列の順序を決定します。
432
521
 
433
- non_selective_attribute_names = {key for key, value in collections.Counter(attribute_name_list).items() if value > self.selective_attribute_value_max_count}
434
- if len(non_selective_attribute_names) > 0:
435
- logger.debug(f"以下の属性は値の個数が{self.selective_attribute_value_max_count}を超えていたため、集計しません。 :: {non_selective_attribute_names}")
436
-
437
- return [(label, attribute_name, attribute_value) for (label, attribute_name, attribute_value) in columns if (label, attribute_name) not in non_selective_attribute_names]
522
+ Args:
523
+ counter_list: アノテーション集計情報のコレクション
524
+ prior_attribute_columns: 優先的に配置する属性値のキーのリスト。Noneの場合はソート順で配置されます。
438
525
 
439
- def _value_columns(self, counter_list: Collection[AnnotationCounter], prior_attribute_columns: list[AttributeValueKey] | None) -> list[AttributeValueKey]:
526
+ Returns:
527
+ 属性値のキーのリスト(CSV列の順序)
528
+ """
440
529
  all_attr_key_set = {attr_key for c in counter_list for attr_key in c.annotation_count_by_attribute}
441
530
  if prior_attribute_columns is not None:
442
531
  remaining_columns = sorted(all_attr_key_set - set(prior_attribute_columns))
443
- remaining_columns_only_selective_attribute = self._only_selective_attribute(remaining_columns)
444
532
 
445
- # `remaining_columns_only_selective_attribute`には、属性値が空である列などが格納されている
446
- # `remaining_columns_only_selective_attribute`を、`value_columns`の関連している位置に挿入する。
533
+ # `remaining_columns`には、属性値が空である列などが格納されている
534
+ # `remaining_columns`を、`value_columns`の関連している位置に挿入する。
447
535
  value_columns = copy.deepcopy(prior_attribute_columns)
448
- for remaining_column in remaining_columns_only_selective_attribute:
536
+ for remaining_column in remaining_columns:
449
537
  is_inserted = False
450
538
  for i in range(len(value_columns) - 1, -1, -1):
451
539
  col = value_columns[i]
@@ -456,11 +544,11 @@ class AttributeCountCsv:
456
544
  if not is_inserted:
457
545
  value_columns.append(remaining_column)
458
546
 
459
- assert len(value_columns) == len(prior_attribute_columns) + len(remaining_columns_only_selective_attribute)
547
+ assert len(value_columns) == len(prior_attribute_columns) + len(remaining_columns)
460
548
 
461
549
  else:
462
550
  remaining_columns = sorted(all_attr_key_set)
463
- value_columns = self._only_selective_attribute(remaining_columns)
551
+ value_columns = remaining_columns
464
552
 
465
553
  # 重複している場合は、重複要素を取り除く。ただし元の順番は維持する
466
554
  value_columns = list(dict.fromkeys(value_columns).keys())
@@ -472,13 +560,22 @@ class AttributeCountCsv:
472
560
  output_file: Path,
473
561
  prior_attribute_columns: list[AttributeValueKey] | None = None,
474
562
  ) -> None:
563
+ """
564
+ タスク単位の属性値ごとアノテーション数をCSVファイルに出力します。
565
+
566
+ Args:
567
+ counter_list: タスク単位のアノテーション集計情報のリスト
568
+ output_file: 出力先CSVファイルのパス
569
+ prior_attribute_columns: 優先的に配置する属性値のキーのリスト
570
+ """
571
+
475
572
  def get_columns() -> list[AttributeValueKey]:
476
573
  basic_columns = [
477
574
  ("project_id", "", ""),
478
575
  ("task_id", "", ""),
479
- ("task_status", "", ""),
480
576
  ("task_phase", "", ""),
481
577
  ("task_phase_stage", "", ""),
578
+ ("task_status", "", ""),
482
579
  ("input_data_count", "", ""),
483
580
  ("annotation_count", "", ""),
484
581
  ]
@@ -512,13 +609,22 @@ class AttributeCountCsv:
512
609
  output_file: Path,
513
610
  prior_attribute_columns: list[AttributeValueKey] | None = None,
514
611
  ) -> None:
612
+ """
613
+ 入力データ単位の属性値ごとアノテーション数をCSVファイルに出力します。
614
+
615
+ Args:
616
+ counter_list: 入力データ単位のアノテーション集計情報のリスト
617
+ output_file: 出力先CSVファイルのパス
618
+ prior_attribute_columns: 優先的に配置する属性値のキーのリスト
619
+ """
620
+
515
621
  def get_columns() -> list[AttributeValueKey]:
516
622
  basic_columns = [
517
623
  ("project_id", "", ""),
518
624
  ("task_id", "", ""),
519
- ("task_status", "", ""),
520
625
  ("task_phase", "", ""),
521
626
  ("task_phase_stage", "", ""),
627
+ ("task_status", "", ""),
522
628
  ("input_data_id", "", ""),
523
629
  ("input_data_name", "", ""),
524
630
  ("frame_no", "", ""),
@@ -563,6 +669,16 @@ class LabelCountCsv:
563
669
  """
564
670
 
565
671
  def _value_columns(self, counter_list: Collection[AnnotationCounter], prior_label_columns: list[str] | None) -> list[str]:
672
+ """
673
+ CSV出力のためのラベル列の順序を決定します。
674
+
675
+ Args:
676
+ counter_list: アノテーション集計情報のコレクション
677
+ prior_label_columns: 優先的に配置するラベル名のリスト。Noneの場合はソート順で配置されます。
678
+
679
+ Returns:
680
+ ラベル名のリスト(CSV列の順序)
681
+ """
566
682
  all_attr_key_set = {attr_key for c in counter_list for attr_key in c.annotation_count_by_label}
567
683
  if prior_label_columns is not None:
568
684
  remaining_columns = sorted(all_attr_key_set - set(prior_label_columns))
@@ -579,13 +695,22 @@ class LabelCountCsv:
579
695
  output_file: Path,
580
696
  prior_label_columns: list[str] | None = None,
581
697
  ) -> None:
698
+ """
699
+ タスク単位のラベルごとアノテーション数をCSVファイルに出力します。
700
+
701
+ Args:
702
+ counter_list: タスク単位のアノテーション集計情報のリスト
703
+ output_file: 出力先CSVファイルのパス
704
+ prior_label_columns: 優先的に配置するラベル名のリスト
705
+ """
706
+
582
707
  def get_columns() -> list[str]:
583
708
  basic_columns = [
584
709
  "project_id",
585
710
  "task_id",
586
- "task_status",
587
711
  "task_phase",
588
712
  "task_phase_stage",
713
+ "task_status",
589
714
  "input_data_count",
590
715
  "annotation_count",
591
716
  ]
@@ -619,13 +744,22 @@ class LabelCountCsv:
619
744
  output_file: Path,
620
745
  prior_label_columns: list[str] | None = None,
621
746
  ) -> None:
747
+ """
748
+ 入力データ単位のラベルごとアノテーション数をCSVファイルに出力します。
749
+
750
+ Args:
751
+ counter_list: 入力データ単位のアノテーション集計情報のリスト
752
+ output_file: 出力先CSVファイルのパス
753
+ prior_label_columns: 優先的に配置するラベル名のリスト
754
+ """
755
+
622
756
  def get_columns() -> list[str]:
623
757
  basic_columns = [
624
758
  "project_id",
625
759
  "task_id",
626
- "task_status",
627
760
  "task_phase",
628
761
  "task_phase_stage",
762
+ "task_status",
629
763
  "input_data_id",
630
764
  "input_data_name",
631
765
  "frame_no",
@@ -736,16 +870,15 @@ class AnnotationSpecs:
736
870
 
737
871
  return result
738
872
 
739
- def selective_attribute_value_keys(self) -> list[AttributeValueKey]:
873
+ def get_attribute_value_keys_for_target_attributes(self, target_attribute_names: Collection[AttributeNameKey]) -> list[AttributeValueKey]:
740
874
  """
741
- 選択系の属性の属性値のキーの一覧。
875
+ 指定された属性名のキーに対応する属性値のキーの一覧を取得します。
742
876
 
743
- 以下の属性種類
744
- * ドロップダウン
745
- * ラジオボタン
746
- * チェックボックス
877
+ Args:
878
+ target_attribute_names: 属性値のキーを取得する対象の属性名のキー。
747
879
  """
748
880
  target_attribute_value_keys = []
881
+ target_attribute_names_set = set(target_attribute_names)
749
882
  for label in self._labels_v1:
750
883
  label_name_en = AddProps.get_message(label["label_name"], MessageLocale.EN)
751
884
  label_name_en = label_name_en if label_name_en is not None else ""
@@ -754,20 +887,33 @@ class AnnotationSpecs:
754
887
  attribute_name_en = AddProps.get_message(attribute["name"], MessageLocale.EN)
755
888
  attribute_name_en = attribute_name_en if attribute_name_en is not None else ""
756
889
 
757
- if AdditionalDataDefinitionType(attribute["type"]) in [
758
- AdditionalDataDefinitionType.CHOICE,
759
- AdditionalDataDefinitionType.SELECT,
760
- ]:
890
+ if (label_name_en, attribute_name_en) not in target_attribute_names_set:
891
+ continue
892
+
893
+ attribute_type = AdditionalDataDefinitionType(attribute["type"])
894
+ if attribute_type in [AdditionalDataDefinitionType.CHOICE, AdditionalDataDefinitionType.SELECT]:
761
895
  for choice in attribute["choices"]:
762
896
  choice_name_en = AddProps.get_message(choice["name"], MessageLocale.EN)
763
897
  choice_name_en = choice_name_en if choice_name_en is not None else ""
764
898
  target_attribute_value_keys.append((label_name_en, attribute_name_en, choice_name_en))
765
899
 
766
- elif AdditionalDataDefinitionType(attribute["type"]) == AdditionalDataDefinitionType.FLAG:
900
+ elif attribute_type == AdditionalDataDefinitionType.FLAG:
767
901
  target_attribute_value_keys.extend([(label_name_en, attribute_name_en, "true"), (label_name_en, attribute_name_en, "false")])
768
902
 
769
- else:
770
- continue
903
+ return target_attribute_value_keys
904
+
905
+ def selective_attribute_value_keys(self) -> list[AttributeValueKey]:
906
+ """
907
+ 選択系(ドロップダウン/ラジオボタン/チェックボックス/フラグ)の属性の属性値のキーの一覧。
908
+
909
+ 対象となる属性種類:
910
+ * ドロップダウン
911
+ * ラジオボタン
912
+ * チェックボックス
913
+ * フラグ
914
+ """
915
+ selective_attribute_names = self.selective_attribute_name_keys()
916
+ target_attribute_value_keys = self.get_attribute_value_keys_for_target_attributes(selective_attribute_names)
771
917
 
772
918
  duplicated_attributes = [key for key, value in collections.Counter(target_attribute_value_keys).items() if value > 1]
773
919
  if len(duplicated_attributes) > 0:
@@ -802,6 +948,34 @@ class AnnotationSpecs:
802
948
 
803
949
  return target_attributes_columns
804
950
 
951
+ def get_attribute_name_keys_by_attribute_names(self, attribute_names: Collection[str]) -> tuple[list[AttributeNameKey], list[str]]:
952
+ """
953
+ 指定された属性名を持つ全てのラベルと属性の組み合わせを取得します。
954
+
955
+ Args:
956
+ attribute_names: 検索対象の属性名のリスト
957
+
958
+ Returns:
959
+ (見つかった属性名キーのリスト, 見つからなかった属性名のリスト) のタプル
960
+ """
961
+ attribute_names_set = set(attribute_names)
962
+ result = []
963
+ found_attribute_names = set()
964
+ for label in self._labels_v1:
965
+ label_name_en = AddProps.get_message(label["label_name"], MessageLocale.EN)
966
+ label_name_en = label_name_en if label_name_en is not None else ""
967
+
968
+ for attribute in label["additional_data_definitions"]:
969
+ attribute_name_en = AddProps.get_message(attribute["name"], MessageLocale.EN)
970
+ attribute_name_en = attribute_name_en if attribute_name_en is not None else ""
971
+
972
+ if attribute_name_en in attribute_names_set:
973
+ result.append((label_name_en, attribute_name_en))
974
+ found_attribute_names.add(attribute_name_en)
975
+
976
+ not_found_attribute_names = list(attribute_names_set - found_attribute_names)
977
+ return result, not_found_attribute_names
978
+
805
979
  def non_selective_attribute_name_keys(self) -> list[AttributeNameKey]:
806
980
  """
807
981
  非選択系の属性の名前のキー
@@ -834,11 +1008,27 @@ class AnnotationSpecs:
834
1008
 
835
1009
 
836
1010
  class ListAnnotationCountMain:
837
- def __init__(self, service: annofabapi.Resource) -> None:
1011
+ def __init__(self, service: annofabapi.Resource, annotation_specs: AnnotationSpecs) -> None:
1012
+ """
1013
+ Args:
1014
+ service: AnnofabのAPIリソース
1015
+ annotation_specs: アノテーション仕様
1016
+ """
838
1017
  self.service = service
1018
+ self.annotation_specs = annotation_specs
839
1019
 
840
1020
  @staticmethod
841
1021
  def get_frame_no_map(task_json_path: Path) -> dict[tuple[str, str], int]:
1022
+ """
1023
+ タスクJSONファイルから、(task_id, input_data_id)をキーとし、フレーム番号を値とするマップを取得します。
1024
+ フレーム番号は1始まりです。
1025
+
1026
+ Args:
1027
+ task_json_path: タスクJSONファイルのパス
1028
+
1029
+ Returns:
1030
+ key: (task_id, input_data_id)のタプル、value: フレーム番号のdict
1031
+ """
842
1032
  with task_json_path.open(encoding="utf-8") as f:
843
1033
  task_list = json.load(f)
844
1034
 
@@ -857,20 +1047,52 @@ class ListAnnotationCountMain:
857
1047
  csv_type: CsvType,
858
1048
  output_file: Path,
859
1049
  *,
860
- project_id: str | None = None,
861
1050
  task_json_path: Path | None = None,
862
1051
  target_task_ids: Collection[str] | None = None,
863
1052
  task_query: TaskQuery | None = None,
1053
+ additional_attribute_names: Collection[AttributeNameKey] | None = None,
1054
+ specified_attribute_names: Collection[AttributeNameKey] | None = None,
864
1055
  ) -> None:
865
- # アノテーション仕様の非選択系の属性は、集計しないようにする。集計しても意味がないため。
866
- annotation_specs: AnnotationSpecs | None = None
867
- non_selective_attribute_name_keys: list[AttributeNameKey] | None = None
868
- if project_id is not None:
869
- annotation_specs = AnnotationSpecs(self.service, project_id)
870
- non_selective_attribute_name_keys = annotation_specs.non_selective_attribute_name_keys()
1056
+ """
1057
+ ラベルごと/属性ごとのアノテーション数を入力データ単位でCSVファイルに出力します。
1058
+
1059
+ Args:
1060
+ annotation_path: アノテーションzipまたはzipを展開したディレクトリのパス
1061
+ csv_type: 出力するCSVの種類(ラベルごと/属性ごと)
1062
+ output_file: 出力先のCSVファイルパス
1063
+ task_json_path: タスクJSONファイルのパス。フレーム番号を出力する場合に指定します。
1064
+ target_task_ids: 集計対象のタスクIDのコレクション
1065
+ task_query: 集計対象タスクを絞り込むためのクエリ条件
1066
+ additional_attribute_names: デフォルトの選択系属性に加えて集計対象とする属性名のキー
1067
+ specified_attribute_names: 集計対象とする属性名のキー。指定した場合、これらの属性のみが集計対象になります。
1068
+
1069
+ Raises:
1070
+ ValueError: `additional_attribute_names`と`specified_attribute_names`の両方が指定された場合に発生します。
1071
+ """
1072
+ if additional_attribute_names is not None and specified_attribute_names is not None:
1073
+ raise ValueError("`additional_attribute_names`と`specified_attribute_names`の両方が指定されています。")
1074
+
1075
+ if specified_attribute_names is not None:
1076
+ # specified_attribute_namesから属性名のみを抽出
1077
+ target_attribute_names_only = list({attr_name for _, attr_name in specified_attribute_names})
1078
+ elif additional_attribute_names is not None:
1079
+ # デフォルトの選択系属性の属性名を取得
1080
+ default_selective_attributes = self.annotation_specs.selective_attribute_name_keys()
1081
+ default_attribute_names = {attr_name for _, attr_name in default_selective_attributes}
1082
+ # additional_attribute_namesから属性名のみを抽出
1083
+ additional_attr_names = {attr_name for _, attr_name in additional_attribute_names}
1084
+ # 両方を組み合わせる
1085
+ target_attribute_names_only = list(default_attribute_names | additional_attr_names)
1086
+ else:
1087
+ # デフォルトでは選択肢系の属性のみを集計対象とする
1088
+ default_selective_attributes = self.annotation_specs.selective_attribute_name_keys()
1089
+ target_attribute_names_only = list({attr_name for _, attr_name in default_selective_attributes})
871
1090
 
872
1091
  frame_no_map = self.get_frame_no_map(task_json_path) if task_json_path is not None else None
873
- counter_by_input_data = ListAnnotationCounterByInputData(non_target_attribute_names=non_selective_attribute_name_keys, frame_no_map=frame_no_map)
1092
+ counter_by_input_data = ListAnnotationCounterByInputData(
1093
+ target_attribute_names_only=target_attribute_names_only,
1094
+ frame_no_map=frame_no_map,
1095
+ )
874
1096
  counter_list_by_input_data = counter_by_input_data.get_annotation_counter_list(
875
1097
  annotation_path,
876
1098
  target_task_ids=target_task_ids,
@@ -879,16 +1101,19 @@ class ListAnnotationCountMain:
879
1101
 
880
1102
  if csv_type == CsvType.LABEL:
881
1103
  # ラベル名の列順が、アノテーション仕様にあるラベル名の順番に対応するようにする。
882
- label_columns: list[str] | None = None
883
- if annotation_specs is not None:
884
- label_columns = annotation_specs.label_keys()
885
-
1104
+ label_columns = self.annotation_specs.label_keys()
886
1105
  LabelCountCsv().print_csv_by_input_data(counter_list_by_input_data, output_file, prior_label_columns=label_columns)
887
1106
  elif csv_type == CsvType.ATTRIBUTE:
888
- attribute_columns: list[AttributeValueKey] | None = None
889
- if annotation_specs is not None:
890
- attribute_columns = annotation_specs.selective_attribute_value_keys()
891
-
1107
+ if specified_attribute_names is not None:
1108
+ # 指定された属性名に対応する属性値のキーのみを取得
1109
+ attribute_columns = self.annotation_specs.get_attribute_value_keys_for_target_attributes(specified_attribute_names)
1110
+ elif additional_attribute_names is not None:
1111
+ # デフォルトの選択系属性とadditional_attribute_namesを組み合わせた属性値のキーを取得
1112
+ default_selective_attributes = self.annotation_specs.selective_attribute_name_keys()
1113
+ combined_attributes = list(set(default_selective_attributes) | set(additional_attribute_names))
1114
+ attribute_columns = self.annotation_specs.get_attribute_value_keys_for_target_attributes(combined_attributes)
1115
+ else:
1116
+ attribute_columns = self.annotation_specs.selective_attribute_value_keys()
892
1117
  AttributeCountCsv().print_csv_by_input_data(counter_list_by_input_data, output_file, prior_attribute_columns=attribute_columns)
893
1118
 
894
1119
  def print_annotation_counter_csv_by_task(
@@ -897,18 +1122,48 @@ class ListAnnotationCountMain:
897
1122
  csv_type: CsvType,
898
1123
  output_file: Path,
899
1124
  *,
900
- project_id: str | None = None,
901
1125
  target_task_ids: Collection[str] | None = None,
902
1126
  task_query: TaskQuery | None = None,
1127
+ additional_attribute_names: Collection[AttributeNameKey] | None = None,
1128
+ specified_attribute_names: Collection[AttributeNameKey] | None = None,
903
1129
  ) -> None:
904
- # アノテーション仕様の非選択系の属性は、集計しないようにする。集計しても意味がないため。
905
- annotation_specs: AnnotationSpecs | None = None
906
- non_selective_attribute_name_keys: list[AttributeNameKey] | None = None
907
- if project_id is not None:
908
- annotation_specs = AnnotationSpecs(self.service, project_id)
909
- non_selective_attribute_name_keys = annotation_specs.non_selective_attribute_name_keys()
910
-
911
- counter_list_by_task = ListAnnotationCounterByTask(non_target_attribute_names=non_selective_attribute_name_keys).get_annotation_counter_list(
1130
+ """
1131
+ ラベルごと/属性ごとのアノテーション数をタスク単位でCSVファイルに出力します。
1132
+
1133
+ Args:
1134
+ annotation_path: アノテーションzipまたはzipを展開したディレクトリのパス
1135
+ csv_type: 出力するCSVの種類(ラベルごと/属性ごと)
1136
+ output_file: 出力先のCSVファイルパス
1137
+ target_task_ids: 集計対象のタスクIDのコレクション
1138
+ task_query: 集計対象タスクを絞り込むためのクエリ条件
1139
+ additional_attribute_names: デフォルトの選択系属性に加えて集計対象とする属性名のキー
1140
+ specified_attribute_names: 集計対象とする属性名のキー。指定した場合、これらの属性のみが集計対象になります。
1141
+
1142
+ Raises:
1143
+ ValueError: `additional_attribute_names`と`specified_attribute_names`の両方が指定された場合に発生します。
1144
+ """
1145
+ if additional_attribute_names is not None and specified_attribute_names is not None:
1146
+ raise ValueError("`additional_attribute_names`と`specified_attribute_names`の両方が指定されています。")
1147
+
1148
+ if specified_attribute_names is not None:
1149
+ # specified_attribute_namesから属性名のみを抽出
1150
+ target_attribute_names_only = list({attr_name for _, attr_name in specified_attribute_names})
1151
+ elif additional_attribute_names is not None:
1152
+ # デフォルトの選択系属性の属性名を取得
1153
+ default_selective_attributes = self.annotation_specs.selective_attribute_name_keys()
1154
+ default_attribute_names = {attr_name for _, attr_name in default_selective_attributes}
1155
+ # additional_attribute_namesから属性名のみを抽出
1156
+ additional_attr_names = {attr_name for _, attr_name in additional_attribute_names}
1157
+ # 両方を組み合わせる
1158
+ target_attribute_names_only = list(default_attribute_names | additional_attr_names)
1159
+ else:
1160
+ # デフォルトでは選択肢系の属性のみを集計対象とする
1161
+ default_selective_attributes = self.annotation_specs.selective_attribute_name_keys()
1162
+ target_attribute_names_only = list({attr_name for _, attr_name in default_selective_attributes})
1163
+
1164
+ counter_list_by_task = ListAnnotationCounterByTask(
1165
+ target_attribute_names_only=target_attribute_names_only,
1166
+ ).get_annotation_counter_list(
912
1167
  annotation_path,
913
1168
  target_task_ids=target_task_ids,
914
1169
  task_query=task_query,
@@ -916,18 +1171,21 @@ class ListAnnotationCountMain:
916
1171
 
917
1172
  if csv_type == CsvType.LABEL:
918
1173
  # 列順が、アノテーション仕様にあるラベル名の順番に対応するようにする。
919
- label_columns: list[str] | None = None
920
- if annotation_specs is not None:
921
- label_columns = annotation_specs.label_keys()
922
-
1174
+ label_columns = self.annotation_specs.label_keys()
923
1175
  LabelCountCsv().print_csv_by_task(counter_list_by_task, output_file, prior_label_columns=label_columns)
924
1176
 
925
1177
  elif csv_type == CsvType.ATTRIBUTE:
926
1178
  # 列順が、アノテーション仕様にある属性名と属性値の順番に対応するようにする。
927
- attribute_columns: list[AttributeValueKey] | None = None
928
- if annotation_specs is not None:
929
- attribute_columns = annotation_specs.selective_attribute_value_keys()
930
-
1179
+ if specified_attribute_names is not None:
1180
+ # 指定された属性名に対応する属性値のキーのみを取得
1181
+ attribute_columns = self.annotation_specs.get_attribute_value_keys_for_target_attributes(specified_attribute_names)
1182
+ elif additional_attribute_names is not None:
1183
+ # デフォルトの選択系属性とadditional_attribute_namesを組み合わせた属性値のキーを取得
1184
+ default_selective_attributes = self.annotation_specs.selective_attribute_name_keys()
1185
+ combined_attributes = list(set(default_selective_attributes) | set(additional_attribute_names))
1186
+ attribute_columns = self.annotation_specs.get_attribute_value_keys_for_target_attributes(combined_attributes)
1187
+ else:
1188
+ attribute_columns = self.annotation_specs.selective_attribute_value_keys()
931
1189
  AttributeCountCsv().print_csv_by_task(counter_list_by_task, output_file, prior_attribute_columns=attribute_columns)
932
1190
 
933
1191
  def print_annotation_counter_json_by_input_data(
@@ -935,23 +1193,54 @@ class ListAnnotationCountMain:
935
1193
  annotation_path: Path,
936
1194
  output_file: Path,
937
1195
  *,
938
- project_id: str | None = None,
939
1196
  task_json_path: Path | None = None,
940
1197
  target_task_ids: Collection[str] | None = None,
941
1198
  task_query: TaskQuery | None = None,
942
1199
  json_is_pretty: bool = False,
1200
+ additional_attribute_names: Collection[AttributeNameKey] | None = None,
1201
+ specified_attribute_names: Collection[AttributeNameKey] | None = None,
943
1202
  ) -> None:
944
- """ラベルごと/属性ごとのアノテーション数を入力データ単位でJSONファイルに出力します。"""
1203
+ """
1204
+ ラベルごと/属性ごとのアノテーション数を入力データ単位でJSONファイルに出力します。
945
1205
 
946
- # アノテーション仕様の非選択系の属性は、集計しないようにする。集計しても意味がないため。
947
- if project_id is not None:
948
- annotation_specs = AnnotationSpecs(self.service, project_id)
949
- non_selective_attribute_name_keys = annotation_specs.non_selective_attribute_name_keys()
1206
+ Args:
1207
+ annotation_path: アノテーションzipまたはzipを展開したディレクトリのパス
1208
+ output_file: 出力先のJSONファイルパス
1209
+ task_json_path: タスクJSONファイルのパス。フレーム番号を出力する場合に指定します。
1210
+ target_task_ids: 集計対象のタスクIDのコレクション
1211
+ task_query: 集計対象タスクを絞り込むためのクエリ条件
1212
+ json_is_pretty: Trueの場合、整形されたJSON(pretty print)を出力します
1213
+ additional_attribute_names: デフォルトの選択系属性に加えて集計対象とする属性名のキー
1214
+ specified_attribute_names: 集計対象とする属性名のキー。指定した場合、これらの属性のみが集計対象になります。
1215
+
1216
+ Raises:
1217
+ ValueError: `additional_attribute_names`と`specified_attribute_names`の両方が指定された場合に発生します。
1218
+ """
1219
+ if additional_attribute_names is not None and specified_attribute_names is not None:
1220
+ raise ValueError("`additional_attribute_names`と`specified_attribute_names`の両方が指定されています。")
1221
+
1222
+ if specified_attribute_names is not None:
1223
+ # specified_attribute_namesから属性名のみを抽出
1224
+ target_attribute_names_only = list({attr_name for _, attr_name in specified_attribute_names})
1225
+ elif additional_attribute_names is not None:
1226
+ # デフォルトの選択系属性の属性名を取得
1227
+ default_selective_attributes = self.annotation_specs.selective_attribute_name_keys()
1228
+ default_attribute_names = {attr_name for _, attr_name in default_selective_attributes}
1229
+ # additional_attribute_namesから属性名のみを抽出
1230
+ additional_attr_names = {attr_name for _, attr_name in additional_attribute_names}
1231
+ # 両方を組み合わせる
1232
+ target_attribute_names_only = list(default_attribute_names | additional_attr_names)
950
1233
  else:
951
- non_selective_attribute_name_keys = None
1234
+ # デフォルトでは選択肢系の属性のみを集計対象とする
1235
+ default_selective_attributes = self.annotation_specs.selective_attribute_name_keys()
1236
+ target_attribute_names_only = list({attr_name for _, attr_name in default_selective_attributes})
952
1237
 
953
1238
  frame_no_map = self.get_frame_no_map(task_json_path) if task_json_path is not None else None
954
- counter_list_by_input_data = ListAnnotationCounterByInputData(non_target_attribute_names=non_selective_attribute_name_keys, frame_no_map=frame_no_map).get_annotation_counter_list(
1239
+ counter_by_input_data = ListAnnotationCounterByInputData(
1240
+ target_attribute_names_only=target_attribute_names_only,
1241
+ frame_no_map=frame_no_map,
1242
+ )
1243
+ counter_list_by_input_data = counter_by_input_data.get_annotation_counter_list(
955
1244
  annotation_path,
956
1245
  target_task_ids=target_task_ids,
957
1246
  task_query=task_query,
@@ -968,22 +1257,48 @@ class ListAnnotationCountMain:
968
1257
  annotation_path: Path,
969
1258
  output_file: Path,
970
1259
  *,
971
- project_id: str | None = None,
972
1260
  target_task_ids: Collection[str] | None = None,
973
1261
  task_query: TaskQuery | None = None,
974
1262
  json_is_pretty: bool = False,
1263
+ additional_attribute_names: Collection[AttributeNameKey] | None = None,
1264
+ specified_attribute_names: Collection[AttributeNameKey] | None = None,
975
1265
  ) -> None:
976
- """ラベルごと/属性ごとのアノテーション数をタスク単位でJSONファイルに出力します。"""
1266
+ """
1267
+ ラベルごと/属性ごとのアノテーション数をタスク単位でJSONファイルに出力します。
977
1268
 
978
- # アノテーション仕様の非選択系の属性は、集計しないようにする。集計しても意味がないため。
979
- if project_id is not None:
980
- annotation_specs = AnnotationSpecs(self.service, project_id)
981
- non_selective_attribute_name_keys = annotation_specs.non_selective_attribute_name_keys()
1269
+ Args:
1270
+ annotation_path: アノテーションzipまたはzipを展開したディレクトリのパス
1271
+ output_file: 出力先のJSONファイルパス
1272
+ target_task_ids: 集計対象のタスクIDのコレクション
1273
+ task_query: 集計対象タスクを絞り込むためのクエリ条件
1274
+ json_is_pretty: Trueの場合、整形されたJSON(pretty print)を出力します
1275
+ additional_attribute_names: デフォルトの選択系属性に加えて集計対象とする属性名のキー
1276
+ specified_attribute_names: 集計対象とする属性名のキー。指定した場合、これらの属性のみが集計対象になります。
1277
+
1278
+ Raises:
1279
+ ValueError: `additional_attribute_names`と`specified_attribute_names`の両方が指定された場合に発生します。
1280
+ """
1281
+ if additional_attribute_names is not None and specified_attribute_names is not None:
1282
+ raise ValueError("`additional_attribute_names`と`specified_attribute_names`の両方が指定されています。")
1283
+
1284
+ if specified_attribute_names is not None:
1285
+ # specified_attribute_namesから属性名のみを抽出
1286
+ target_attribute_names_only = list({attr_name for _, attr_name in specified_attribute_names})
1287
+ elif additional_attribute_names is not None:
1288
+ # デフォルトの選択系属性の属性名を取得
1289
+ default_selective_attributes = self.annotation_specs.selective_attribute_name_keys()
1290
+ default_attribute_names = {attr_name for _, attr_name in default_selective_attributes}
1291
+ # additional_attribute_namesから属性名のみを抽出
1292
+ additional_attr_names = {attr_name for _, attr_name in additional_attribute_names}
1293
+ # 両方を組み合わせる
1294
+ target_attribute_names_only = list(default_attribute_names | additional_attr_names)
982
1295
  else:
983
- non_selective_attribute_name_keys = None
1296
+ # デフォルトでは選択肢系の属性のみを集計対象とする
1297
+ default_selective_attributes = self.annotation_specs.selective_attribute_name_keys()
1298
+ target_attribute_names_only = list({attr_name for _, attr_name in default_selective_attributes})
984
1299
 
985
1300
  counter_list_by_task = ListAnnotationCounterByTask(
986
- non_target_attribute_names=non_selective_attribute_name_keys,
1301
+ target_attribute_names_only=target_attribute_names_only,
987
1302
  ).get_annotation_counter_list(
988
1303
  annotation_path,
989
1304
  target_task_ids=target_task_ids,
@@ -1003,34 +1318,51 @@ class ListAnnotationCountMain:
1003
1318
  output_file: Path,
1004
1319
  arg_format: OutputFormat,
1005
1320
  *,
1006
- project_id: str | None = None,
1007
1321
  task_json_path: Path | None = None,
1008
1322
  target_task_ids: Collection[str] | None = None,
1009
1323
  task_query: TaskQuery | None = None,
1010
1324
  csv_type: CsvType | None = None,
1325
+ additional_attribute_names: Collection[AttributeNameKey] | None = None,
1326
+ specified_attribute_names: Collection[AttributeNameKey] | None = None,
1011
1327
  ) -> None:
1012
- """ラベルごと/属性ごとのアノテーション数を出力します。"""
1328
+ """
1329
+ ラベルごと/属性ごとのアノテーション数を出力します。
1330
+
1331
+ Args:
1332
+ annotation_path: アノテーションzipまたはzipを展開したディレクトリのパス
1333
+ group_by: 集計単位(タスク単位または入力データ単位)
1334
+ output_file: 出力先ファイルのパス
1335
+ arg_format: 出力形式(CSV、JSON、整形済みJSON)
1336
+ task_json_path: タスクJSONファイルのパス。フレーム番号を出力する場合に指定します。
1337
+ target_task_ids: 集計対象のタスクIDのコレクション
1338
+ task_query: 集計対象タスクを絞り込むためのクエリ条件
1339
+ csv_type: CSV出力時の種類(ラベルごと/属性ごと)。CSV形式の場合は必須です。
1340
+ additional_attribute_names: デフォルトの選択系属性に加えて集計対象とする属性名のキー
1341
+ specified_attribute_names: 集計対象とする属性名のキー。指定した場合、これらの属性のみが集計対象になります。
1342
+ """
1013
1343
  if arg_format == OutputFormat.CSV:
1014
1344
  assert csv_type is not None
1015
1345
  if group_by == GroupBy.INPUT_DATA_ID:
1016
1346
  self.print_annotation_counter_csv_by_input_data(
1017
- project_id=project_id,
1018
1347
  annotation_path=annotation_path,
1019
1348
  task_json_path=task_json_path,
1020
1349
  output_file=output_file,
1350
+ csv_type=csv_type,
1021
1351
  target_task_ids=target_task_ids,
1022
1352
  task_query=task_query,
1023
- csv_type=csv_type,
1353
+ additional_attribute_names=additional_attribute_names,
1354
+ specified_attribute_names=specified_attribute_names,
1024
1355
  )
1025
1356
 
1026
1357
  elif group_by == GroupBy.TASK_ID:
1027
1358
  self.print_annotation_counter_csv_by_task(
1028
- project_id=project_id,
1029
1359
  annotation_path=annotation_path,
1030
1360
  output_file=output_file,
1361
+ csv_type=csv_type,
1031
1362
  target_task_ids=target_task_ids,
1032
1363
  task_query=task_query,
1033
- csv_type=csv_type,
1364
+ additional_attribute_names=additional_attribute_names,
1365
+ specified_attribute_names=specified_attribute_names,
1034
1366
  )
1035
1367
 
1036
1368
  elif arg_format in [OutputFormat.PRETTY_JSON, OutputFormat.JSON]:
@@ -1038,23 +1370,25 @@ class ListAnnotationCountMain:
1038
1370
 
1039
1371
  if group_by == GroupBy.INPUT_DATA_ID:
1040
1372
  self.print_annotation_counter_json_by_input_data(
1041
- project_id=project_id,
1042
1373
  annotation_path=annotation_path,
1043
1374
  task_json_path=task_json_path,
1044
1375
  output_file=output_file,
1045
1376
  target_task_ids=target_task_ids,
1046
1377
  task_query=task_query,
1047
1378
  json_is_pretty=json_is_pretty,
1379
+ additional_attribute_names=additional_attribute_names,
1380
+ specified_attribute_names=specified_attribute_names,
1048
1381
  )
1049
1382
 
1050
1383
  elif group_by == GroupBy.TASK_ID:
1051
1384
  self.print_annotation_counter_json_by_task(
1052
- project_id=project_id,
1053
1385
  annotation_path=annotation_path,
1054
1386
  output_file=output_file,
1055
1387
  target_task_ids=target_task_ids,
1056
1388
  task_query=task_query,
1057
1389
  json_is_pretty=json_is_pretty,
1390
+ additional_attribute_names=additional_attribute_names,
1391
+ specified_attribute_names=specified_attribute_names,
1058
1392
  )
1059
1393
 
1060
1394
 
@@ -1063,55 +1397,61 @@ class ListAnnotationCount(CommandLine):
1063
1397
  アノテーション数情報を出力する。
1064
1398
  """
1065
1399
 
1066
- COMMON_MESSAGE = "annofabcli statistics list_annotation_count: error:"
1067
-
1068
- def validate(self, args: argparse.Namespace) -> bool:
1069
- if args.project_id is None and args.annotation is None:
1070
- print( # noqa: T201
1071
- f"{self.COMMON_MESSAGE} argument --project_id: '--annotation'が未指定のときは、'--project_id' を指定してください。",
1072
- file=sys.stderr,
1073
- )
1074
- return False
1075
-
1076
- return True
1077
-
1078
1400
  def main(self) -> None:
1401
+ """
1402
+ アノテーション数情報を出力するメイン処理。
1403
+ コマンドライン引数を解析し、アノテーションzip/ディレクトリから集計情報を取得して、
1404
+ 指定された形式(CSV/JSON)で出力します。
1405
+ """
1079
1406
  args = self.args
1080
1407
 
1081
- if not self.validate(args):
1082
- sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
1408
+ project_id: str = args.project_id
1409
+ super().validate_project(project_id, project_member_roles=[ProjectMemberRole.OWNER, ProjectMemberRole.TRAINING_DATA_USER])
1083
1410
 
1084
- project_id: str | None = args.project_id
1085
- if project_id is not None:
1086
- super().validate_project(project_id, project_member_roles=[ProjectMemberRole.OWNER, ProjectMemberRole.TRAINING_DATA_USER])
1087
-
1088
- annotation_path = Path(args.annotation) if args.annotation is not None else None
1411
+ annotation_path = args.annotation
1089
1412
 
1090
1413
  task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id) if args.task_id is not None else None
1091
1414
  task_query = TaskQuery.from_dict(annofabcli.common.cli.get_json_from_args(args.task_query)) if args.task_query is not None else None
1092
1415
 
1416
+ # アノテーション仕様を取得
1417
+ annotation_specs = AnnotationSpecs(self.service, project_id)
1418
+
1419
+ additional_attribute_names: list[AttributeNameKey] | None = None
1420
+ specified_attribute_names: list[AttributeNameKey] | None = None
1421
+
1422
+ if args.additional_attribute_name is not None:
1423
+ attribute_name_str_list = annofabcli.common.cli.get_list_from_args(args.additional_attribute_name)
1424
+ # アノテーション仕様から属性名を検索してAttributeNameKeyのリストに変換
1425
+ additional_attribute_names, not_found_names = annotation_specs.get_attribute_name_keys_by_attribute_names(attribute_name_str_list)
1426
+ if len(not_found_names) > 0:
1427
+ logger.warning(f"指定された属性名のうち、アノテーション仕様に見つからなかった属性名があります。 :: {not_found_names}")
1428
+ elif args.attribute_name is not None:
1429
+ attribute_name_str_list = annofabcli.common.cli.get_list_from_args(args.attribute_name)
1430
+ # アノテーション仕様から属性名を検索してAttributeNameKeyのリストに変換
1431
+ specified_attribute_names, not_found_names = annotation_specs.get_attribute_name_keys_by_attribute_names(attribute_name_str_list)
1432
+ if len(not_found_names) > 0:
1433
+ logger.warning(f"指定された属性名のうち、アノテーション仕様に見つからなかった属性名があります。 :: {not_found_names}")
1434
+
1093
1435
  group_by = GroupBy(args.group_by)
1094
1436
  csv_type = CsvType(args.type)
1095
1437
  output_file: Path = args.output
1096
1438
  arg_format = OutputFormat(args.format)
1097
- main_obj = ListAnnotationCountMain(self.service)
1439
+ main_obj = ListAnnotationCountMain(self.service, annotation_specs)
1098
1440
 
1099
1441
  downloading_obj = DownloadingFile(self.service)
1100
1442
 
1101
1443
  def download_and_process_annotation(temp_dir: Path, *, is_latest: bool, annotation_path: Path | None) -> None:
1102
- # タスク全件ファイルは、フレーム番号を参照するのに利用する
1103
- if project_id is not None:
1444
+ # タスク全件ファイルは、group_by=input_data_idの場合のみフレーム番号を参照するのに利用する
1445
+ task_json_path: Path | None = None
1446
+ if group_by == GroupBy.INPUT_DATA_ID:
1104
1447
  task_json_path = downloading_obj.download_task_json_to_dir(
1105
1448
  project_id,
1106
1449
  temp_dir,
1107
1450
  is_latest=is_latest,
1108
1451
  )
1109
- else:
1110
- task_json_path = None
1111
1452
 
1112
1453
  func = partial(
1113
1454
  main_obj.print_annotation_counter,
1114
- project_id=project_id,
1115
1455
  task_json_path=task_json_path,
1116
1456
  group_by=group_by,
1117
1457
  csv_type=csv_type,
@@ -1119,10 +1459,11 @@ class ListAnnotationCount(CommandLine):
1119
1459
  output_file=output_file,
1120
1460
  target_task_ids=task_id_list,
1121
1461
  task_query=task_query,
1462
+ additional_attribute_names=additional_attribute_names,
1463
+ specified_attribute_names=specified_attribute_names,
1122
1464
  )
1123
1465
 
1124
1466
  if annotation_path is None:
1125
- assert project_id is not None
1126
1467
  annotation_path = downloading_obj.download_annotation_zip_to_dir(
1127
1468
  project_id,
1128
1469
  temp_dir,
@@ -1132,44 +1473,29 @@ class ListAnnotationCount(CommandLine):
1132
1473
  else:
1133
1474
  func(annotation_path=annotation_path)
1134
1475
 
1135
- if project_id is not None:
1136
- if args.temp_dir is not None:
1137
- download_and_process_annotation(temp_dir=args.temp_dir, is_latest=args.latest, annotation_path=annotation_path)
1138
- else:
1139
- with tempfile.TemporaryDirectory() as str_temp_dir:
1140
- download_and_process_annotation(temp_dir=Path(str_temp_dir), is_latest=args.latest, annotation_path=annotation_path)
1476
+ if args.temp_dir is not None:
1477
+ download_and_process_annotation(temp_dir=args.temp_dir, is_latest=args.latest, annotation_path=annotation_path)
1141
1478
  else:
1142
- # プロジェクトIDが指定されていない場合は、アノテーションパスが必須なので、一時ディレクトリは不要
1143
- assert annotation_path is not None
1144
- func = partial(
1145
- main_obj.print_annotation_counter,
1146
- project_id=project_id,
1147
- task_json_path=None,
1148
- group_by=group_by,
1149
- csv_type=csv_type,
1150
- arg_format=arg_format,
1151
- output_file=output_file,
1152
- target_task_ids=task_id_list,
1153
- task_query=task_query,
1154
- )
1155
- func(annotation_path=annotation_path)
1479
+ with tempfile.TemporaryDirectory() as str_temp_dir:
1480
+ download_and_process_annotation(temp_dir=Path(str_temp_dir), is_latest=args.latest, annotation_path=annotation_path)
1156
1481
 
1157
1482
 
1158
1483
  def parse_args(parser: argparse.ArgumentParser) -> None:
1484
+ """
1485
+ コマンドライン引数のパーサーを設定します。
1486
+
1487
+ Args:
1488
+ parser: argparseのArgumentParserインスタンス
1489
+ """
1159
1490
  argument_parser = ArgumentParser(parser)
1160
1491
 
1161
1492
  parser.add_argument(
1162
1493
  "--annotation",
1163
- type=str,
1494
+ type=Path,
1164
1495
  help="アノテーションzip、またはzipを展開したディレクトリを指定します。指定しない場合はAnnofabからダウンロードします。",
1165
1496
  )
1166
1497
 
1167
- parser.add_argument(
1168
- "-p",
1169
- "--project_id",
1170
- type=str,
1171
- help="project_id。``--annotation`` が未指定のときは必須です。``--annotation`` が指定されているときに ``--project_id`` を指定すると、アノテーション仕様を参照して、集計対象の属性やCSV列順が決まります。", # noqa: E501
1172
- )
1498
+ argument_parser.add_project_id()
1173
1499
 
1174
1500
  parser.add_argument(
1175
1501
  "--group_by",
@@ -1218,16 +1544,53 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
1218
1544
  help="指定したディレクトリに、アノテーションZIPなどの一時ファイルをダウンロードします。",
1219
1545
  )
1220
1546
 
1547
+ # --attribute_nameと--additional_attribute_nameは排他的
1548
+ attribute_group = parser.add_mutually_exclusive_group()
1549
+ attribute_group.add_argument(
1550
+ "--additional_attribute_name",
1551
+ type=str,
1552
+ nargs="+",
1553
+ help="デフォルトで集計される選択肢系の属性(ドロップダウン、ラジオボタン、チェックボックス)に加えて、集計したい属性の英語名を指定します。"
1554
+ "ラベル名に関係なく、デフォルト属性と指定した属性名を持つ属性が集計対象になります。"
1555
+ " ``file://`` を先頭に付けると、属性名が記載されたファイルを指定できます。"
1556
+ " ``--type attribute`` を指定したときのみ有効なオプションです。",
1557
+ )
1558
+
1559
+ attribute_group.add_argument(
1560
+ "--attribute_name",
1561
+ type=str,
1562
+ nargs="+",
1563
+ help="集計対象とする属性の英語名を指定します。指定した属性名のみが集計対象になります(デフォルトの選択肢系属性は含まれません)。"
1564
+ "ラベル名に関係なく、指定した属性名を持つ属性のみが集計対象になります。"
1565
+ " ``file://`` を先頭に付けると、属性名が記載されたファイルを指定できます。"
1566
+ " ``--type attribute`` を指定したときのみ有効なオプションです。",
1567
+ )
1568
+
1221
1569
  parser.set_defaults(subcommand_func=main)
1222
1570
 
1223
1571
 
1224
1572
  def main(args: argparse.Namespace) -> None:
1573
+ """
1574
+ list_annotation_countコマンドのエントリーポイント。
1575
+
1576
+ Args:
1577
+ args: コマンドライン引数
1578
+ """
1225
1579
  service = build_annofabapi_resource_and_login(args)
1226
1580
  facade = AnnofabApiFacade(service)
1227
1581
  ListAnnotationCount(service, facade, args).main()
1228
1582
 
1229
1583
 
1230
1584
  def add_parser(subparsers: argparse._SubParsersAction | None = None) -> argparse.ArgumentParser:
1585
+ """
1586
+ list_annotation_countサブコマンドのパーサーを追加します。
1587
+
1588
+ Args:
1589
+ subparsers: 親パーサーのサブパーサーアクション
1590
+
1591
+ Returns:
1592
+ 作成されたArgumentParserインスタンス
1593
+ """
1231
1594
  subcommand_name = "list_annotation_count"
1232
1595
  subcommand_help = "ラベルごとまたは属性値ごとにアノテーション数を出力します。"
1233
1596
  epilog = "オーナロールまたはアノテーションユーザロールを持つユーザで実行してください。"