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.
- annofabcli/annotation/copy_annotation.py +5 -1
- annofabcli/annotation/delete_annotation.py +14 -3
- annofabcli/annotation/merge_segmentation.py +5 -1
- annofabcli/annotation/remove_segmentation_overlap.py +6 -1
- annofabcli/organization_member/delete_organization_member.py +34 -14
- annofabcli/organization_member/invite_organization_member.py +27 -14
- annofabcli/statistics/list_annotation_area.py +320 -0
- annofabcli/statistics/subcommand_statistics.py +2 -0
- {annofabcli-1.101.0.dist-info → annofabcli-1.102.1.dist-info}/METADATA +1 -1
- {annofabcli-1.101.0.dist-info → annofabcli-1.102.1.dist-info}/RECORD +13 -12
- {annofabcli-1.101.0.dist-info → annofabcli-1.102.1.dist-info}/WHEEL +0 -0
- {annofabcli-1.101.0.dist-info → annofabcli-1.102.1.dist-info}/entry_points.txt +0 -0
- {annofabcli-1.101.0.dist-info → annofabcli-1.102.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
39
|
-
|
|
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"
|
|
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}'
|
|
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"
|
|
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"
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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="
|
|
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,
|
|
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
|
|
33
|
-
|
|
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}'
|
|
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}'
|
|
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}'
|
|
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}'
|
|
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
|
-
|
|
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.
|
|
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="
|
|
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,
|
|
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)
|
|
@@ -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=
|
|
9
|
-
annofabcli/annotation/delete_annotation.py,sha256=
|
|
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=
|
|
16
|
-
annofabcli/annotation/remove_segmentation_overlap.py,sha256=
|
|
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=
|
|
110
|
-
annofabcli/organization_member/invite_organization_member.py,sha256=
|
|
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=
|
|
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.
|
|
210
|
-
annofabcli-1.
|
|
211
|
-
annofabcli-1.
|
|
212
|
-
annofabcli-1.
|
|
213
|
-
annofabcli-1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|