annofabcli 1.101.0__py3-none-any.whl → 1.102.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.
@@ -10,6 +10,7 @@ from dataclasses import dataclass
10
10
  from typing import Any, Optional
11
11
 
12
12
  import annofabapi
13
+ from annofabapi.models import ProjectMemberRole
13
14
  from annofabapi.utils import can_put_annotation
14
15
 
15
16
  import annofabcli
@@ -350,6 +351,8 @@ class CopyAnnotation(CommandLine):
350
351
  sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
351
352
 
352
353
  project_id = args.project_id
354
+ super().validate_project(project_id, [ProjectMemberRole.OWNER, ProjectMemberRole.ACCEPTER, ProjectMemberRole.WORKER])
355
+
353
356
  str_copy_target_list = get_list_from_args(args.input)
354
357
 
355
358
  copy_target_list = get_copy_target_list(str_copy_target_list)
@@ -421,6 +424,7 @@ def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argpa
421
424
  subcommand_name = "copy"
422
425
  subcommand_help = "アノテーションをコピーします."
423
426
  description = "タスク単位または入力データ単位で、アノテーションをコピーします。"
424
- parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description)
427
+ epilog = "オーナー、チェッカーまたはアノテータロールを持つユーザで実行してください。"
428
+ parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog)
425
429
  parse_args(parser)
426
430
  return parser
@@ -356,7 +356,13 @@ class DeleteAnnotation(CommandLine):
356
356
  else:
357
357
  backup_dir = Path(args.backup)
358
358
 
359
- super().validate_project(project_id, [ProjectMemberRole.OWNER])
359
+ if args.force:
360
+ # --forceオプションが指定されている場合は、完了状態のタスクも削除する
361
+ # 完了状態のタスクを削除するには、オーナーロールである必要があるため、`args.force`で条件を分岐する
362
+ super().validate_project(project_id, [ProjectMemberRole.OWNER])
363
+ else:
364
+ super().validate_project(project_id, [ProjectMemberRole.OWNER, ProjectMemberRole.ACCEPTER])
365
+
360
366
  main_obj = DeleteAnnotationMain(self.service, project_id, all_yes=args.yes, is_force=args.force)
361
367
 
362
368
  if args.json is not None:
@@ -452,7 +458,12 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
452
458
  f"(ex): ``{json.dumps(EXAMPLE_ANNOTATION_QUERY)}``",
453
459
  )
454
460
 
455
- parser.add_argument("--force", action="store_true", help="指定した場合は、完了状態のタスクのアノテーションも削除します。")
461
+ parser.add_argument(
462
+ "--force",
463
+ action="store_true",
464
+ help="指定した場合は、完了状態のタスクのアノテーションも削除します。"
465
+ "ただし、完了状態のタスクを削除するには、オーナーロールを持つユーザーが実行する必要があります。",
466
+ )
456
467
  parser.add_argument(
457
468
  "--backup",
458
469
  type=str,
@@ -469,7 +480,7 @@ def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argpa
469
480
  "タスク配下のアノテーションを削除します。ただし、作業中状態のタスクのアノテーションは削除できません。"
470
481
  "間違えてアノテーションを削除したときに復元できるようにするため、 ``--backup`` でバックアップ用のディレクトリを指定することを推奨します。"
471
482
  )
472
- epilog = "オーナロールを持つユーザで実行してください。"
483
+ epilog = "オーナーまたはチェッカーロールを持つユーザで実行してください。ただし``--force``オプションを指定した場合は、オーナーロールを持つユーザで実行してください。" # noqa: E501
473
484
 
474
485
  parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog)
475
486
  parse_args(parser)
@@ -12,6 +12,7 @@ from typing import Any, Optional
12
12
 
13
13
  import annofabapi
14
14
  import numpy
15
+ from annofabapi.models import ProjectMemberRole
15
16
  from annofabapi.pydantic_models.default_annotation_type import DefaultAnnotationType
16
17
  from annofabapi.pydantic_models.task_status import TaskStatus
17
18
  from annofabapi.segmentation import read_binary_image, write_binary_image
@@ -304,6 +305,8 @@ class MergeSegmentation(CommandLine):
304
305
  sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
305
306
 
306
307
  project_id = args.project_id
308
+ super().validate_project(project_id, [ProjectMemberRole.OWNER, ProjectMemberRole.ACCEPTER, ProjectMemberRole.WORKER])
309
+
307
310
  task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id)
308
311
  label_name_list = annofabcli.common.cli.get_list_from_args(args.label_name)
309
312
 
@@ -385,6 +388,7 @@ def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argpa
385
388
  "複数の塗りつぶしアノテーションを1つにまとめます。"
386
389
  "ラベルの種類を「塗りつぶし(インスタンスセグメンテーション)」から「塗りつぶしv2(セマンティックセグメンテーション)」に変更する場合などに有用です。"
387
390
  )
388
- parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description)
391
+ epilog = "オーナー、チェッカーまたはアノテータロールを持つユーザで実行してください。"
392
+ parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog)
389
393
  parse_args(parser)
390
394
  return parser
@@ -12,6 +12,7 @@ from typing import Any, Optional
12
12
 
13
13
  import annofabapi
14
14
  import numpy
15
+ from annofabapi.models import ProjectMemberRole
15
16
  from annofabapi.pydantic_models.task_status import TaskStatus
16
17
  from annofabapi.segmentation import read_binary_image, write_binary_image
17
18
  from annofabapi.utils import can_put_annotation
@@ -291,8 +292,11 @@ class RemoveSegmentationOverlap(CommandLine):
291
292
  sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
292
293
 
293
294
  project_id = args.project_id
295
+
294
296
  task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id)
295
297
 
298
+ super().validate_project(project_id, [ProjectMemberRole.OWNER, ProjectMemberRole.ACCEPTER, ProjectMemberRole.WORKER])
299
+
296
300
  main_obj = RemoveSegmentationOverlapMain(
297
301
  self.service,
298
302
  project_id=project_id,
@@ -338,6 +342,7 @@ def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argpa
338
342
  "Annofabでインスタンスセグメンテーションは重ねることができてしまいます。"
339
343
  "この重なりをなくしたいときに有用です。"
340
344
  )
341
- parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description)
345
+ epilog = "オーナー、チェッカーまたはアノテータロールを持つユーザで実行してください。"
346
+ parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog)
342
347
  parse_args(parser)
343
348
  return parser
@@ -7,6 +7,7 @@ from typing import Any, Optional
7
7
 
8
8
  import annofabapi
9
9
  import more_itertools
10
+ from annofabapi.models import OrganizationMemberRole
10
11
 
11
12
  import annofabcli
12
13
  import annofabcli.common.cli
@@ -35,8 +36,14 @@ class DeleteOrganizationMemberMain(CommandLineWithConfirm):
35
36
  def get_member(organization_member_list: list[dict[str, Any]], user_id: str) -> Optional[dict[str, Any]]:
36
37
  return more_itertools.first_true(organization_member_list, pred=lambda e: e["user_id"] == user_id)
37
38
 
38
- def main(self, organization_name: str, user_ids: Collection[str]) -> None:
39
- logger.info(f"{len(user_ids)} 件のユーザを組織'{organization_name}'から脱退させます。")
39
+ def delete_organization_members_from_organization(self, organization_name: str, user_ids: Collection[str]) -> None:
40
+ if not self.facade.contains_any_organization_member_role(
41
+ organization_name, {OrganizationMemberRole.ADMINISTRATOR, OrganizationMemberRole.OWNER}
42
+ ):
43
+ logger.warning(f"組織'{organization_name}'に所属していないか、組織メンバーを脱退できるロールを持たないため、スキップします。")
44
+ return
45
+
46
+ logger.info(f"{len(user_ids)} 件のメンバーを組織'{organization_name}'から脱退させます。")
40
47
 
41
48
  member_list = self.service.wrapper.get_all_organization_members(organization_name)
42
49
 
@@ -45,24 +52,30 @@ class DeleteOrganizationMemberMain(CommandLineWithConfirm):
45
52
  for user_id in user_ids:
46
53
  member = self.get_member(member_list, user_id)
47
54
  if member is None:
48
- logger.warning(f"組織メンバにuser_id='{user_id}'のユーザが存在しません。")
55
+ logger.warning(f"組織'{organization_name}'に user_id='{user_id}'のメンバーが存在しません。")
49
56
  continue
50
57
 
51
58
  if not self.confirm_processing(
52
- f"user_id='{user_id}'のユーザを組織から脱退させますか? :: username='{member['username']}', role='{member['role']}'"
59
+ f"組織'{organization_name}'に所属する user_id='{user_id}'のメンバーを脱退させますか? :: username='{member['username']}'"
53
60
  ):
54
61
  continue
55
62
 
56
- logger.debug(f"user_id='{user_id}'のユーザを組織から脱退させます。 :: username='{member['username']}', role='{member['role']}'")
57
63
  try:
58
64
  self.service.api.delete_organization_member(organization_name, user_id)
59
- logger.debug(f"user_id='{user_id}'のユーザを組織'{organization_name}'から脱退させました。")
65
+ logger.debug(f"組織'{organization_name}'から user_id='{user_id}'のメンバーを脱退させました。")
60
66
  success_count += 1
61
67
 
62
68
  except Exception: # pylint: disable=broad-except
63
- logger.warning(f"user_id='{user_id}'のユーザを組織'{organization_name}'から脱退させるのに失敗しました。", exc_info=True)
69
+ logger.warning(f"組織'{organization_name}'から user_id='{user_id}'のメンバーを脱退させるのに失敗しました。", exc_info=True)
70
+
71
+ logger.info(f"{success_count} / {len(user_ids)} 件のメンバーを組織'{organization_name}'から脱退させました。")
64
72
 
65
- logger.info(f"{success_count} / {len(user_ids)} 件のユーザを組織'{organization_name}'から脱退させました。")
73
+ def delete_organization_members_from_organizations(self, organization_names: list[str], user_ids: Collection[str]) -> None:
74
+ for organization_name in organization_names:
75
+ try:
76
+ self.delete_organization_members_from_organization(organization_name, user_ids)
77
+ except Exception: # pylint
78
+ logger.warning(f"組織'{organization_name}'からメンバーを脱退させるのに失敗しました。", exc_info=True)
66
79
 
67
80
 
68
81
  class DeleteOrganizationMember(CommandLine):
@@ -70,9 +83,10 @@ class DeleteOrganizationMember(CommandLine):
70
83
  args = self.args
71
84
 
72
85
  user_id_list = annofabcli.common.cli.get_list_from_args(args.user_id)
86
+ organization_name_list = annofabcli.common.cli.get_list_from_args(args.organization)
73
87
 
74
88
  main_obj = DeleteOrganizationMemberMain(self.service, all_yes=args.yes)
75
- main_obj.main(organization_name=args.organization, user_ids=user_id_list)
89
+ main_obj.delete_organization_members_from_organizations(organization_name_list, user_id_list)
76
90
 
77
91
 
78
92
  def main(args: argparse.Namespace) -> None:
@@ -82,7 +96,14 @@ def main(args: argparse.Namespace) -> None:
82
96
 
83
97
 
84
98
  def parse_args(parser: argparse.ArgumentParser) -> None:
85
- parser.add_argument("-org", "--organization", required=True, type=str, help="対象の組織の組織名を指定してください。")
99
+ parser.add_argument(
100
+ "-org",
101
+ "--organization",
102
+ nargs="+",
103
+ required=True,
104
+ type=str,
105
+ help="対象の組織名を指定してます。 ``file://`` を先頭に付けると、一覧が記載されたファイルを指定できます。",
106
+ )
86
107
 
87
108
  parser.add_argument(
88
109
  "-u",
@@ -90,7 +111,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
90
111
  type=str,
91
112
  nargs="+",
92
113
  required=True,
93
- help="組織から脱退させるユーザのuser_idを指定してください。 ``file://`` を先頭に付けると、一覧が記載されたファイルを指定できます。",
114
+ help="組織から脱退させるメンバーのuser_idを指定します。 ``file://`` を先頭に付けると、一覧が記載されたファイルを指定できます。",
94
115
  )
95
116
 
96
117
  parser.set_defaults(subcommand_func=main)
@@ -98,10 +119,9 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
98
119
 
99
120
  def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
100
121
  subcommand_name = "delete"
101
- subcommand_help = "組織からユーザを脱退させます。"
102
- description = "組織からユーザを脱退させます。"
122
+ subcommand_help = "組織からメンバーを脱退させます。"
103
123
  epilog = "組織オーナまたは組織管理者ロールを持つユーザで実行してください。"
104
124
 
105
- parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, command_help=subcommand_help, description=description, epilog=epilog)
125
+ parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, command_help=subcommand_help, epilog=epilog)
106
126
  parse_args(parser)
107
127
  return parser
@@ -29,8 +29,14 @@ class InviteOrganizationMemberMain(CommandLineWithConfirm):
29
29
  self.facade = AnnofabApiFacade(service)
30
30
  super().__init__(all_yes)
31
31
 
32
- def main(self, organization_name: str, user_ids: Collection[str], role: str) -> None:
33
- logger.info(f"{len(user_ids)} 件のユーザを組織'{organization_name}'に招待して、ロール'{role}'を付与します。")
32
+ def invite_members_to_organization(self, organization_name: str, user_ids: Collection[str], role: str) -> None:
33
+ if not self.facade.contains_any_organization_member_role(
34
+ organization_name, {OrganizationMemberRole.ADMINISTRATOR, OrganizationMemberRole.OWNER}
35
+ ):
36
+ logger.warning(f"組織'{organization_name}'に所属していないか、組織メンバーを招待できるロールを持たないため、スキップします。")
37
+ return
38
+
39
+ logger.info(f"{len(user_ids)} 件のメンバーを組織'{organization_name}'に招待して、ロール'{role}'を付与します。")
34
40
 
35
41
  organization_member_list = self.service.wrapper.get_all_organization_members(organization_name)
36
42
  all_user_ids = {e["user_id"] for e in organization_member_list}
@@ -39,21 +45,28 @@ class InviteOrganizationMemberMain(CommandLineWithConfirm):
39
45
  success_count = 0
40
46
  for user_id in user_ids:
41
47
  if user_id in all_user_ids:
42
- logger.warning(f"user_id='{user_id}'のユーザは、すでに組織'{organization_name}'に存在しています。")
48
+ logger.warning(f"user_id='{user_id}'のメンバーは、すでに組織'{organization_name}'に存在しているため、スキップします。")
43
49
  continue
44
50
 
45
- if not self.confirm_processing(f"user_id='{user_id}'のユーザを組織'{organization_name}'に招待しますか? :: role='{role}'"):
51
+ if not self.confirm_processing(f"user_id='{user_id}'のメンバーを組織'{organization_name}'に招待して、ロール'{role}'を付与しますか?"):
46
52
  continue
47
53
 
48
54
  try:
49
55
  self.service.api.invite_organization_member(organization_name, user_id, request_body={"role": role})
50
- logger.debug(f"user_id='{user_id}'のユーザを組織'{organization_name}'に招待しました。")
56
+ logger.debug(f"user_id='{user_id}'のメンバーを組織'{organization_name}'に招待しました。")
51
57
  success_count += 1
52
58
 
53
59
  except Exception: # pylint: disable=broad-except
54
- logger.warning(f"user_id='{user_id}'のユーザを組織'{organization_name}'に招待するのに失敗しました。", exc_info=True)
60
+ logger.warning(f"user_id='{user_id}'のメンバーを組織'{organization_name}'に招待するのに失敗しました。", exc_info=True)
61
+
62
+ logger.info(f"{success_count} / {len(user_ids)} 件のメンバーを組織'{organization_name}'に招待しました。")
55
63
 
56
- logger.info(f"{success_count} / {len(user_ids)} 件のユーザを組織'{organization_name}'に招待しました。")
64
+ def invite_members_to_organizations(self, organization_names: list[str], user_ids: Collection[str], role: str) -> None:
65
+ for organization_name in organization_names:
66
+ try:
67
+ self.invite_members_to_organization(organization_name, user_ids, role)
68
+ except Exception: # pylint
69
+ logger.warning(f"組織'{organization_name}'にメンバーを招待するのに失敗しました。", exc_info=True)
57
70
 
58
71
 
59
72
  class InviteOrganizationMember(CommandLine):
@@ -61,9 +74,10 @@ class InviteOrganizationMember(CommandLine):
61
74
  args = self.args
62
75
 
63
76
  user_id_list = annofabcli.common.cli.get_list_from_args(args.user_id)
77
+ organization_name_list = annofabcli.common.cli.get_list_from_args(args.organization)
64
78
 
65
79
  main_obj = InviteOrganizationMemberMain(self.service, all_yes=args.yes)
66
- main_obj.main(organization_name=args.organization, user_ids=user_id_list, role=args.role)
80
+ main_obj.invite_members_to_organizations(organization_name_list, user_id_list, role=args.role)
67
81
 
68
82
 
69
83
  def main(args: argparse.Namespace) -> None:
@@ -73,7 +87,7 @@ def main(args: argparse.Namespace) -> None:
73
87
 
74
88
 
75
89
  def parse_args(parser: argparse.ArgumentParser) -> None:
76
- parser.add_argument("-org", "--organization", required=True, type=str, help="対象の組織の組織名を指定してください。")
90
+ parser.add_argument("-org", "--organization", nargs="+", required=True, type=str, help="招待先の組織名を指定してください。")
77
91
 
78
92
  parser.add_argument(
79
93
  "-u",
@@ -81,7 +95,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
81
95
  type=str,
82
96
  nargs="+",
83
97
  required=True,
84
- help="組織に招待するユーザのuser_idを指定してください。 ``file://`` を先頭に付けると、一覧が記載されたファイルを指定できます。",
98
+ help="組織に招待するメンバーのuser_idを指定してます。 ``file://`` を先頭に付けると、一覧が記載されたファイルを指定できます。",
85
99
  )
86
100
 
87
101
  role_choices = [e.value for e in OrganizationMemberRole]
@@ -90,7 +104,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
90
104
  type=str,
91
105
  choices=role_choices,
92
106
  required=True,
93
- help="招待するユーザに割り当てる組織メンバのロールを指定してください。",
107
+ help="招待するメンバーに割り当てるロールを指定してください。",
94
108
  )
95
109
 
96
110
  parser.set_defaults(subcommand_func=main)
@@ -98,10 +112,9 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
98
112
 
99
113
  def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
100
114
  subcommand_name = "invite"
101
- subcommand_help = "組織にユーザを招待します。"
102
- description = "組織にユーザを招待します。"
115
+ subcommand_help = "組織にメンバーを招待します。"
103
116
  epilog = "組織オーナまたは組織管理者ロールを持つユーザで実行してください。"
104
117
 
105
- parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, command_help=subcommand_help, description=description, epilog=epilog)
118
+ parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, command_help=subcommand_help, epilog=epilog)
106
119
  parse_args(parser)
107
120
  return parser
@@ -0,0 +1,320 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ import sys
6
+ import tempfile
7
+ import zipfile
8
+ from collections.abc import Collection, Iterator
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Any, Optional
12
+
13
+ import numpy
14
+ import pandas
15
+ from annofabapi.models import InputDataType, ProjectMemberRole
16
+ from annofabapi.parser import (
17
+ SimpleAnnotationParser,
18
+ lazy_parse_simple_annotation_dir,
19
+ lazy_parse_simple_annotation_zip,
20
+ )
21
+ from annofabapi.segmentation import read_binary_image
22
+ from dataclasses_json import DataClassJsonMixin
23
+
24
+ import annofabcli
25
+ import annofabcli.common.cli
26
+ from annofabcli.common.cli import (
27
+ COMMAND_LINE_ERROR_STATUS_CODE,
28
+ ArgumentParser,
29
+ CommandLine,
30
+ build_annofabapi_resource_and_login,
31
+ )
32
+ from annofabcli.common.download import DownloadingFile
33
+ from annofabcli.common.enums import FormatArgument
34
+ from annofabcli.common.facade import (
35
+ AnnofabApiFacade,
36
+ TaskQuery,
37
+ match_annotation_with_task_query,
38
+ )
39
+ from annofabcli.common.utils import print_csv, print_json
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ def lazy_parse_simple_annotation_by_input_data(annotation_path: Path) -> Iterator[SimpleAnnotationParser]:
45
+ if not annotation_path.exists():
46
+ raise RuntimeError(f"'{annotation_path}' は存在しません。")
47
+
48
+ if annotation_path.is_dir():
49
+ return lazy_parse_simple_annotation_dir(annotation_path)
50
+ elif zipfile.is_zipfile(str(annotation_path)):
51
+ return lazy_parse_simple_annotation_zip(annotation_path)
52
+ else:
53
+ raise RuntimeError(f"'{annotation_path}'は、zipファイルまたはディレクトリではありません。")
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class AnnotationAreaInfo(DataClassJsonMixin):
58
+ task_id: str
59
+ task_status: str
60
+ task_phase: str
61
+ task_phase_stage: int
62
+
63
+ input_data_id: str
64
+ input_data_name: str
65
+
66
+ label: str
67
+ annotation_id: str
68
+ annotation_area: int
69
+
70
+
71
+ def calculate_bounding_box_area(data: dict[str, Any]) -> int:
72
+ width = abs(data["right_bottom"]["x"] - data["left_top"]["x"])
73
+ height = abs(data["right_bottom"]["y"] - data["left_top"]["y"])
74
+ return width * height
75
+
76
+
77
+ def calculate_segmentation_area(outer_file: Path) -> int:
78
+ nd_array = read_binary_image(outer_file)
79
+ return numpy.count_nonzero(nd_array)
80
+
81
+
82
+ def get_annotation_area_info_list(parser: SimpleAnnotationParser, simple_annotation: dict[str, Any]) -> list[AnnotationAreaInfo]:
83
+ result = []
84
+ for detail in simple_annotation["details"]:
85
+ if detail["data"]["_type"] in {"Segmentation", "SegmentationV2"}:
86
+ annotation_area = calculate_segmentation_area(parser.open_outer_file(detail["data"]["data_uri"]))
87
+ elif detail["data"]["_type"] == "BoundingBox":
88
+ annotation_area = calculate_bounding_box_area(detail["data"])
89
+
90
+ else:
91
+ continue
92
+
93
+ result.append(
94
+ AnnotationAreaInfo(
95
+ task_id=simple_annotation["task_id"],
96
+ task_phase=simple_annotation["task_phase"],
97
+ task_phase_stage=simple_annotation["task_phase_stage"],
98
+ task_status=simple_annotation["task_status"],
99
+ input_data_id=simple_annotation["input_data_id"],
100
+ input_data_name=simple_annotation["input_data_name"],
101
+ label=detail["label"],
102
+ annotation_id=detail["annotation_id"],
103
+ annotation_area=annotation_area,
104
+ )
105
+ )
106
+
107
+ return result
108
+
109
+
110
+ def get_annotation_area_info_list_from_annotation_path(
111
+ annotation_path: Path,
112
+ *,
113
+ target_task_ids: Optional[Collection[str]] = None,
114
+ task_query: Optional[TaskQuery] = None,
115
+ ) -> list[AnnotationAreaInfo]:
116
+ annotation_area_list = []
117
+ target_task_ids = set(target_task_ids) if target_task_ids is not None else None
118
+ iter_parser = lazy_parse_simple_annotation_by_input_data(annotation_path)
119
+ logger.debug("アノテーションzip/ディレクトリを読み込み中")
120
+ for index, parser in enumerate(iter_parser):
121
+ if (index + 1) % 1000 == 0:
122
+ logger.debug(f"{index + 1} 件目のJSONを読み込み中")
123
+ if target_task_ids is not None and parser.task_id not in target_task_ids:
124
+ continue
125
+ simple_annotation_dict = parser.load_json()
126
+ if task_query is not None: # noqa: SIM102
127
+ if not match_annotation_with_task_query(simple_annotation_dict, task_query):
128
+ continue
129
+ sub_annotation_area_list = get_annotation_area_info_list(parser, simple_annotation_dict)
130
+ annotation_area_list.extend(sub_annotation_area_list)
131
+ return annotation_area_list
132
+
133
+
134
+ def create_df(
135
+ annotation_area_list: list[AnnotationAreaInfo],
136
+ ) -> pandas.DataFrame:
137
+ columns = [
138
+ "task_id",
139
+ "task_status",
140
+ "task_phase",
141
+ "task_phase_stage",
142
+ "input_data_id",
143
+ "input_data_name",
144
+ "label",
145
+ "annotation_id",
146
+ "annotation_area",
147
+ ]
148
+ df = pandas.DataFrame(e.to_dict() for e in annotation_area_list)
149
+ if len(df) == 0:
150
+ df = pandas.DataFrame(columns=columns)
151
+
152
+ df = df.fillna({"annotation_area": 0})
153
+ return df[columns]
154
+
155
+
156
+ def print_annotation_area(
157
+ annotation_path: Path,
158
+ output_file: Path,
159
+ output_format: FormatArgument,
160
+ *,
161
+ target_task_ids: Optional[Collection[str]] = None,
162
+ task_query: Optional[TaskQuery] = None,
163
+ ) -> None:
164
+ annotation_area_list = get_annotation_area_info_list_from_annotation_path(
165
+ annotation_path,
166
+ target_task_ids=target_task_ids,
167
+ task_query=task_query,
168
+ )
169
+
170
+ logger.info(f"{len(annotation_area_list)} 件のタスクに含まれる塗りつぶしアノテーションのピクセル数情報を出力します。")
171
+
172
+ if output_format == FormatArgument.CSV:
173
+ df = create_df(annotation_area_list)
174
+ print_csv(df, output_file)
175
+
176
+ elif output_format in [FormatArgument.PRETTY_JSON, FormatArgument.JSON]:
177
+ json_is_pretty = output_format == FormatArgument.PRETTY_JSON
178
+ print_json(
179
+ [e.to_dict(encode_json=True) for e in annotation_area_list],
180
+ is_pretty=json_is_pretty,
181
+ output=output_file,
182
+ )
183
+
184
+ else:
185
+ raise RuntimeError(f"出力形式 '{output_format}' はサポートされていません。")
186
+
187
+
188
+ class ListAnnotationArea(CommandLine):
189
+ COMMON_MESSAGE = "annofabcli statistics list_annotation_area: error:"
190
+
191
+ def validate(self, args: argparse.Namespace) -> bool:
192
+ if args.project_id is None and args.annotation is None:
193
+ print( # noqa: T201
194
+ f"{self.COMMON_MESSAGE} argument --project_id: '--annotation'が未指定のときは、'--project_id' を指定してください。",
195
+ file=sys.stderr,
196
+ )
197
+ return False
198
+ return True
199
+
200
+ def main(self) -> None:
201
+ args = self.args
202
+
203
+ if not self.validate(args):
204
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
205
+
206
+ project_id: Optional[str] = args.project_id
207
+ if project_id is not None:
208
+ super().validate_project(project_id, project_member_roles=[ProjectMemberRole.OWNER, ProjectMemberRole.TRAINING_DATA_USER])
209
+ project, _ = self.service.api.get_project(project_id)
210
+ if project["input_data_type"] != InputDataType.IMAGE.value:
211
+ logger.warning(f"project_id='{project_id}'であるプロジェクトは、画像プロジェクトでないので、出力されるデータは0件になります。")
212
+
213
+ annotation_path = Path(args.annotation) if args.annotation is not None else None
214
+
215
+ task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id) if args.task_id is not None else None
216
+ task_query = TaskQuery.from_dict(annofabcli.common.cli.get_json_from_args(args.task_query)) if args.task_query is not None else None
217
+
218
+ output_file: Path = args.output
219
+ output_format = FormatArgument(args.format)
220
+
221
+ downloading_obj = DownloadingFile(self.service)
222
+
223
+ def download_and_print_annotation_area(project_id: str, temp_dir: Path, *, is_latest: bool, annotation_path: Optional[Path]) -> None:
224
+ if annotation_path is None:
225
+ annotation_path = temp_dir / f"{project_id}__annotation.zip"
226
+ downloading_obj.download_annotation_zip(
227
+ project_id,
228
+ dest_path=annotation_path,
229
+ is_latest=is_latest,
230
+ )
231
+ print_annotation_area(
232
+ output_format=output_format,
233
+ output_file=output_file,
234
+ target_task_ids=task_id_list,
235
+ task_query=task_query,
236
+ annotation_path=annotation_path,
237
+ )
238
+
239
+ if project_id is not None:
240
+ if args.temp_dir is not None:
241
+ download_and_print_annotation_area(
242
+ project_id=project_id, temp_dir=args.temp_dir, is_latest=args.latest, annotation_path=annotation_path
243
+ )
244
+ else:
245
+ with tempfile.TemporaryDirectory() as str_temp_dir:
246
+ download_and_print_annotation_area(
247
+ project_id=project_id, temp_dir=Path(str_temp_dir), is_latest=args.latest, annotation_path=annotation_path
248
+ )
249
+ else:
250
+ assert annotation_path is not None
251
+ print_annotation_area(
252
+ output_format=output_format,
253
+ output_file=output_file,
254
+ target_task_ids=task_id_list,
255
+ task_query=task_query,
256
+ annotation_path=annotation_path,
257
+ )
258
+
259
+
260
+ def parse_args(parser: argparse.ArgumentParser) -> None:
261
+ argument_parser = ArgumentParser(parser)
262
+
263
+ parser.add_argument(
264
+ "--annotation",
265
+ type=str,
266
+ help="アノテーションzip、またはzipを展開したディレクトリを指定します。指定しない場合はAnnofabからダウンロードします。",
267
+ )
268
+
269
+ parser.add_argument(
270
+ "-p",
271
+ "--project_id",
272
+ type=str,
273
+ help="project_id。``--annotation`` が未指定のときは必須です。\n"
274
+ "``--annotation`` が指定されているときに ``--project_id`` を指定すると、アノテーション仕様を参照して、集計対象の属性やCSV列順が決まります。",
275
+ )
276
+
277
+ argument_parser.add_format(
278
+ choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON],
279
+ default=FormatArgument.CSV,
280
+ )
281
+
282
+ argument_parser.add_output()
283
+
284
+ parser.add_argument(
285
+ "-tq",
286
+ "--task_query",
287
+ type=str,
288
+ help="集計対象タスクを絞り込むためのクエリ条件をJSON形式で指定します。使用できるキーは task_id, status, phase, phase_stage です。"
289
+ " ``file://`` を先頭に付けると、JSON形式のファイルを指定できます。",
290
+ )
291
+ argument_parser.add_task_id(required=False)
292
+
293
+ parser.add_argument(
294
+ "--latest",
295
+ action="store_true",
296
+ help="``--annotation`` を指定しないとき、最新のアノテーションzipを参照します。このオプションを指定すると、アノテーションzipを更新するのに数分待ちます。", # noqa: E501
297
+ )
298
+
299
+ parser.add_argument(
300
+ "--temp_dir",
301
+ type=Path,
302
+ help="指定したディレクトリに、アノテーションZIPなどの一時ファイルをダウンロードします。",
303
+ )
304
+
305
+ parser.set_defaults(subcommand_func=main)
306
+
307
+
308
+ def main(args: argparse.Namespace) -> None:
309
+ service = build_annofabapi_resource_and_login(args)
310
+ facade = AnnofabApiFacade(service)
311
+ ListAnnotationArea(service, facade, args).main()
312
+
313
+
314
+ def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
315
+ subcommand_name = "list_annotation_area"
316
+ subcommand_help = "塗りつぶしアノテーションまたは矩形アノテーションの面積を出力します。"
317
+ epilog = "オーナロールまたはアノテーションユーザロールを持つユーザで実行してください。"
318
+ parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description=subcommand_help, epilog=epilog)
319
+ parse_args(parser)
320
+ return parser
@@ -4,6 +4,7 @@ from typing import Optional
4
4
  import annofabcli
5
5
  import annofabcli.common.cli
6
6
  import annofabcli.stat_visualization.merge_visualization_dir
7
+ import annofabcli.statistics.list_annotation_area
7
8
  import annofabcli.statistics.list_annotation_attribute
8
9
  import annofabcli.statistics.list_annotation_attribute_filled_count
9
10
  import annofabcli.statistics.list_annotation_count
@@ -27,6 +28,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
27
28
  annofabcli.statistics.list_annotation_attribute_filled_count.add_parser(subparsers)
28
29
  annofabcli.statistics.list_annotation_count.add_parser(subparsers)
29
30
  annofabcli.statistics.list_annotation_duration.add_parser(subparsers)
31
+ annofabcli.statistics.list_annotation_area.add_parser(subparsers)
30
32
 
31
33
  annofabcli.statistics.list_video_duration.add_parser(subparsers)
32
34
  annofabcli.statistics.list_worktime.add_parser(subparsers)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: annofabcli
3
- Version: 1.101.0
3
+ Version: 1.102.1
4
4
  Summary: Utility Command Line Interface for AnnoFab
5
5
  Author: Kurusugawa Computer Inc.
6
6
  License: MIT
@@ -5,15 +5,15 @@ annofabcli/annotation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
5
5
  annofabcli/annotation/annotation_query.py,sha256=ke3W3RT1-WfFzwt-TXcQwGmghG34vcKJkM_jxgbNKjU,15922
6
6
  annofabcli/annotation/change_annotation_attributes.py,sha256=GcabzuTeUhGHibYxOhoSgB9RR1ACmtrdxdmGGCiUnNQ,17200
7
7
  annofabcli/annotation/change_annotation_properties.py,sha256=Zgxl2GVuoMsFComW5Wkaip3UKeOyPpH9T0FMsngpswc,18427
8
- annofabcli/annotation/copy_annotation.py,sha256=Zy8sS7Hy4DVPLgtRrppEVuesfzj4rq07vXM783k6Akg,18165
9
- annofabcli/annotation/delete_annotation.py,sha256=1rwuPTd_bnrmq_pVrzojy-1i7nAAme6MaTQEfKmq4n0,22696
8
+ annofabcli/annotation/copy_annotation.py,sha256=79kJOfz_U_PIcP89HXdMW0HN5wiN5i-AxdjkwFgg5rQ,18482
9
+ annofabcli/annotation/delete_annotation.py,sha256=pZb48d7lRYEAYOFPOi_pt3rnOG5YNGuq-oPbjTOFEXc,23462
10
10
  annofabcli/annotation/download_annotation_zip.py,sha256=P_ZpdqIaSFEmB8jjpdykcRhh2tVlHxSlXFrYreJjShE,3282
11
11
  annofabcli/annotation/dump_annotation.py,sha256=bVEQeRYfskEKo5QjZ-bT6zsBon8u4bRySVy-CmJJTm8,7493
12
12
  annofabcli/annotation/import_annotation.py,sha256=Yg032TQgnBVUaG12Kjvta2YLvSk4NN9CvdZTWtZta28,33799
13
13
  annofabcli/annotation/list_annotation.py,sha256=B8ZFI7SRQPy-TgbpaX-tzXhveViY17YuCPE9BuK9mrs,10790
14
14
  annofabcli/annotation/list_annotation_count.py,sha256=T9fbaoxWeDJIVgW_YgHRldbwrVZWiE-57lfJrDQrj80,6474
15
- annofabcli/annotation/merge_segmentation.py,sha256=ldEQlcyCrog0KFYn92sTkScZQ7gmTjlujuCsKBemjhU,17950
16
- annofabcli/annotation/remove_segmentation_overlap.py,sha256=gONYqaI0PMEGYJduB4JsmGBn3LCY9JtLSFgosWYyc3M,15957
15
+ annofabcli/annotation/merge_segmentation.py,sha256=FuUrDG6HqMOna4BDJtC-NpqUBdQn45Oi-hzFyAVtDng,18267
16
+ annofabcli/annotation/remove_segmentation_overlap.py,sha256=NIwz45DwuBO6p-Zsty1WdI4eAJbzqUJkDLiJFZirX2k,16275
17
17
  annofabcli/annotation/restore_annotation.py,sha256=B3zitm-xB5ZuZy7c-6umiltK49Gdu3Di7YWTqNhJ22g,14704
18
18
  annofabcli/annotation/subcommand_annotation.py,sha256=ku9mzb7zZilHcjf1MFV1E7EJ8OvfSUDHpcunM38teto,2122
19
19
  annofabcli/annotation_specs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -106,8 +106,8 @@ annofabcli/organization/list_organization.py,sha256=kafkiBHd4nkJ_fqxJwYcuU79mqzE
106
106
  annofabcli/organization/subcommand_organization.py,sha256=EM-3uU45T9H51kcQGVR6a-mmU9nLd9akP2-tULq2RY0,719
107
107
  annofabcli/organization_member/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
108
108
  annofabcli/organization_member/change_organization_member.py,sha256=DrRnVHTXRp5kE8ezgkXofuL0tgpvEPxwF8MowCDv27Q,4918
109
- annofabcli/organization_member/delete_organization_member.py,sha256=n_TI5oLUwWi3UzaffrtmwoVvsQhqh_KocxCPxAc3JXM,4304
110
- annofabcli/organization_member/invite_organization_member.py,sha256=LndGMPOSTn5nlnu_Gg8XUcXe4oG6-31jbPjv7Gz9h0k,4249
109
+ annofabcli/organization_member/delete_organization_member.py,sha256=tHMHMxNUpM-wpGP7ycyMh5lmifKMD802FHEuCRMIQNo,5305
110
+ annofabcli/organization_member/invite_organization_member.py,sha256=5sixK8WqUGl7DAOyIRxQ-uf106FYMHl4k7VtF1SatfQ,5177
111
111
  annofabcli/organization_member/list_organization_member.py,sha256=zaZECajTok6N_HJvH2xAwB62qMOJjaIwabTdTfTwMJY,3588
112
112
  annofabcli/organization_member/subcommand_organization_member.py,sha256=iK-Y4PxG5J8fu0Bnf8NBixv81BRXvr3BiVUyUb3ET7A,1289
113
113
  annofabcli/project/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -135,6 +135,7 @@ annofabcli/stat_visualization/write_performance_rating_csv.py,sha256=TDn7-poyFt2
135
135
  annofabcli/statistics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
136
136
  annofabcli/statistics/histogram.py,sha256=CvzDxT2cKLSnBGSqkZE6p92PayGxYYja1YyB24M4ALU,3245
137
137
  annofabcli/statistics/linegraph.py,sha256=0kr7jVBNMiM2ECYhv3Ry5RitElKerSl9ZKxbKzfiplI,12494
138
+ annofabcli/statistics/list_annotation_area.py,sha256=1LFYqc1JodVuHKM2ceTwWriDCabMmkOnHiNptvEyXAw,12325
138
139
  annofabcli/statistics/list_annotation_attribute.py,sha256=87jjNCOXJUbWnmswMCLN7GTjGsBfqpFJ6hViWmnj8Y4,12557
139
140
  annofabcli/statistics/list_annotation_attribute_filled_count.py,sha256=vwWeFHwTnEMdrLBauIKPFDkUCa6lXXd0GQgUAQ0LCqU,28890
140
141
  annofabcli/statistics/list_annotation_count.py,sha256=nzmlHRCWt5mjeksZkeQyWqm4UaCa9SrdbNtuX9TPP5w,52907
@@ -142,7 +143,7 @@ annofabcli/statistics/list_annotation_duration.py,sha256=N7nnVUDfX_thIapqe6-z_Mq
142
143
  annofabcli/statistics/list_video_duration.py,sha256=uNeMteRBX2JG_AWmcgMJj0Jzbq_qF7bvAwr25GmeIiw,9124
143
144
  annofabcli/statistics/list_worktime.py,sha256=C7Yu3IOW2EvhkJJv6gY3hNdS9_TOLmT_9LZsB7vLJ1o,6493
144
145
  annofabcli/statistics/scatter.py,sha256=IUCwXix9GbZb6V82wjjb5q2eamrT5HQsU_bzDTjAFnM,11011
145
- annofabcli/statistics/subcommand_statistics.py,sha256=mx18Fgxz2eG4LrF-x0vISw2qh9aLommxuQLD8cfoZhw,2416
146
+ annofabcli/statistics/subcommand_statistics.py,sha256=Pvd7s0vvDU9tSpAphPrv94IDhhR1p8iFH2tjdt7I7ZU,2536
146
147
  annofabcli/statistics/summarize_task_count.py,sha256=8OH6BBRYRjHJkWRTjU0A0OfXa7f3NIRHrxPNFlRt_hM,9707
147
148
  annofabcli/statistics/summarize_task_count_by_task_id_group.py,sha256=TSSmcFv615NLcq6uqXmg3ilYqSHl3A5qp90msVQM1gE,8646
148
149
  annofabcli/statistics/summarize_task_count_by_user.py,sha256=TRoJXpt2HOVb8QO2YtRejkOAxyK80_NsPt3Vk9es9C8,6948
@@ -206,8 +207,8 @@ annofabcli/task_history_event/download_task_history_event_json.py,sha256=hQLVbQ0
206
207
  annofabcli/task_history_event/list_all_task_history_event.py,sha256=JQEgwOIXbbTIfeX23AVaoySHViOR9UGm9uoXuhVEBqo,6446
207
208
  annofabcli/task_history_event/list_worktime.py,sha256=9jsRYa2C9bva8E1Aqxv9CCKDuCP0MvbiaIyQFTDpjqY,13150
208
209
  annofabcli/task_history_event/subcommand_task_history_event.py,sha256=mJVJoT4RXk4HWnY7-Nrsl4If-gtaIIEXd2z7eFZwM2I,1260
209
- annofabcli-1.101.0.dist-info/METADATA,sha256=zlNu7ev7fOJfYzE4Dx2Gs6B_tT2_kvIOwBBI10WAzyg,5286
210
- annofabcli-1.101.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
211
- annofabcli-1.101.0.dist-info/entry_points.txt,sha256=C2uSUc-kkLJpoK_mDL5FEMAdorLEMPfwSf8VBMYnIFM,56
212
- annofabcli-1.101.0.dist-info/licenses/LICENSE,sha256=pcqWYfxFtxBzhvKp3x9MXNM4xciGb2eFewaRhXUNHlo,1081
213
- annofabcli-1.101.0.dist-info/RECORD,,
210
+ annofabcli-1.102.1.dist-info/METADATA,sha256=dagbVH22HM5jYGQFjhCTeWBOGeFpZKnzfYMix15O8VU,5286
211
+ annofabcli-1.102.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
212
+ annofabcli-1.102.1.dist-info/entry_points.txt,sha256=C2uSUc-kkLJpoK_mDL5FEMAdorLEMPfwSf8VBMYnIFM,56
213
+ annofabcli-1.102.1.dist-info/licenses/LICENSE,sha256=pcqWYfxFtxBzhvKp3x9MXNM4xciGb2eFewaRhXUNHlo,1081
214
+ annofabcli-1.102.1.dist-info/RECORD,,