annofabcli 1.108.0__py3-none-any.whl → 1.110.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. annofabcli/annotation_zip/list_annotation_bounding_box_2d.py +49 -37
  2. annofabcli/annotation_zip/list_range_annotation.py +308 -0
  3. annofabcli/annotation_zip/subcommand_annotation_zip.py +4 -0
  4. annofabcli/annotation_zip/validate_annotation.py +393 -0
  5. annofabcli/common/download.py +97 -0
  6. annofabcli/project/create_project.py +151 -0
  7. annofabcli/project/put_project.py +14 -129
  8. annofabcli/project/subcommand_project.py +4 -0
  9. annofabcli/project/update_project.py +298 -0
  10. annofabcli/statistics/list_annotation_area.py +2 -3
  11. annofabcli/statistics/list_annotation_attribute_filled_count.py +35 -10
  12. annofabcli/statistics/list_annotation_count.py +39 -14
  13. annofabcli/statistics/list_annotation_duration.py +4 -6
  14. annofabcli/statistics/summarize_task_count.py +53 -33
  15. annofabcli/statistics/summarize_task_count_by_task_id_group.py +30 -13
  16. annofabcli/statistics/summarize_task_count_by_user.py +32 -15
  17. annofabcli/statistics/visualization/dataframe/annotation_count.py +31 -3
  18. annofabcli/statistics/visualization/dataframe/annotation_duration.py +121 -0
  19. annofabcli/statistics/visualize_statistics.py +83 -5
  20. annofabcli/task/complete_tasks.py +2 -2
  21. {annofabcli-1.108.0.dist-info → annofabcli-1.110.0.dist-info}/METADATA +1 -1
  22. {annofabcli-1.108.0.dist-info → annofabcli-1.110.0.dist-info}/RECORD +25 -20
  23. {annofabcli-1.108.0.dist-info → annofabcli-1.110.0.dist-info}/WHEEL +0 -0
  24. {annofabcli-1.108.0.dist-info → annofabcli-1.110.0.dist-info}/entry_points.txt +0 -0
  25. {annofabcli-1.108.0.dist-info → annofabcli-1.110.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,151 +1,36 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
- import logging
5
4
  import sys
6
- import uuid
7
- from enum import Enum
8
- from typing import Any, Optional
9
-
10
- from annofabapi.models import InputDataType
11
- from annofabapi.plugin import EditorPluginId, ExtendSpecsPluginId
5
+ from logging import getLogger
6
+ from typing import Optional
12
7
 
13
8
  import annofabcli
14
- from annofabcli.common.cli import (
15
- COMMAND_LINE_ERROR_STATUS_CODE,
16
- CommandLine,
17
- build_annofabapi_resource_and_login,
18
- get_json_from_args,
19
- )
20
- from annofabcli.common.facade import AnnofabApiFacade
21
-
22
- logger = logging.getLogger(__name__)
23
-
24
-
25
- class CustomProjectType(Enum):
26
- """
27
- カスタムプロジェクトの種類
28
- """
29
-
30
- THREE_DIMENSION = "3d"
31
- """3次元データ"""
32
-
33
-
34
- class PutProject(CommandLine):
35
- def put_project( # noqa: ANN201
36
- self,
37
- organization: str,
38
- title: str,
39
- input_data_type: InputDataType,
40
- *,
41
- project_id: Optional[str],
42
- overview: Optional[str],
43
- editor_plugin_id: Optional[str],
44
- custom_project_type: Optional[CustomProjectType],
45
- configuration: Optional[dict[str, Any]],
46
- ):
47
- new_project_id = project_id if project_id is not None else str(uuid.uuid4())
48
- if configuration is None:
49
- configuration = {}
50
-
51
- if input_data_type == InputDataType.CUSTOM and custom_project_type is not None:
52
- assert editor_plugin_id is None
53
- editor_plugin_id = EditorPluginId.THREE_DIMENSION.value
54
- configuration.update({"extended_specs_plugin_id": ExtendSpecsPluginId.THREE_DIMENSION.value})
55
-
56
- configuration.update({"plugin_id": editor_plugin_id})
57
-
58
- request_body = {
59
- "title": title,
60
- "organization_name": organization,
61
- "input_data_type": input_data_type.value,
62
- "overview": overview,
63
- "status": "active",
64
- "configuration": configuration,
65
- }
66
- new_project, _ = self.service.api.put_project(new_project_id, request_body=request_body)
67
- logger.info(
68
- f"'{organization}'組織に、project_id='{new_project['project_id']}'のプロジェクトを作成しました。 :: title='{new_project['title']}', input_data_type='{new_project['input_data_type']}'"
69
- )
9
+ from annofabcli.project import create_project
70
10
 
71
- COMMON_MESSAGE = "annofabcli project put: error:"
72
-
73
- def validate(self, args: argparse.Namespace) -> bool:
74
- if args.input_data_type == InputDataType.CUSTOM.value: # noqa: SIM102
75
- if args.plugin_id is None and args.custom_project_type is None:
76
- print( # noqa: T201
77
- f"{self.COMMON_MESSAGE} '--input_data_type custom' を指定した場合は、'--plugin_id' または '--custom_project_type' が必須です。",
78
- file=sys.stderr,
79
- )
80
- return False
81
-
82
- return True
83
-
84
- def main(self) -> None:
85
- args = self.args
86
- if not self.validate(args):
87
- sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
88
-
89
- self.put_project(
90
- args.organization,
91
- args.title,
92
- InputDataType(args.input_data_type),
93
- project_id=args.project_id,
94
- overview=args.overview,
95
- editor_plugin_id=args.plugin_id,
96
- custom_project_type=CustomProjectType(args.custom_project_type) if args.custom_project_type is not None else None,
97
- configuration=get_json_from_args(args.configuration),
98
- )
11
+ logger = getLogger(__name__)
99
12
 
100
13
 
101
14
  def main(args: argparse.Namespace) -> None:
102
- service = build_annofabapi_resource_and_login(args)
103
- facade = AnnofabApiFacade(service)
104
- PutProject(service, facade, args).main()
15
+ print("[DEPRECATED] :: `project put` コマンドは非推奨です。代わりに `project create` コマンドを使用してください。`project put` コマンドは2026年01月01日以降に廃止予定です。", file=sys.stderr) # noqa: T201
16
+ # create_project.py の実装を使用
17
+ create_project.main(args)
105
18
 
106
19
 
107
20
  def parse_args(parser: argparse.ArgumentParser) -> None:
108
- parser.add_argument("-org", "--organization", type=str, required=True, help="プロジェクトの所属先組織")
109
-
110
- parser.add_argument("--title", type=str, required=True, help="作成するプロジェクトのタイトル")
111
- parser.add_argument(
112
- "--input_data_type",
113
- type=str,
114
- choices=[e.value for e in InputDataType],
115
- required=True,
116
- help=f"プロジェクトに登録する入力データの種類\n\n * {InputDataType.IMAGE.value} : 画像\n * {InputDataType.MOVIE.value} : 動画\n * {InputDataType.CUSTOM.value} : カスタム(点群など)",
117
- )
118
-
119
- parser.add_argument("-p", "--project_id", type=str, required=False, help="作成するプロジェクトのproject_id。未指定の場合はUUIDv4になります。")
120
- parser.add_argument("--overview", type=str, help="作成するプロジェクトの概要")
121
-
122
- group = parser.add_mutually_exclusive_group()
123
- group.add_argument("--plugin_id", type=str, help="アノテーションエディタプラグインのplugin_id")
124
- group.add_argument(
125
- "--custom_project_type",
126
- type=str,
127
- choices=[e.value for e in CustomProjectType],
128
- help="カスタムプロジェクトの種類。 ``--input_data_type custom`` を指定したときのみ有効です。"
129
- "指定した値に対応するエディタプラグインが適用されるため、 `--plugin_id`` と同時には指定できません。\n"
130
- " * 3d : 3次元データ",
131
- )
132
-
133
- parser.add_argument(
134
- "--configuration",
135
- type=str,
136
- help="プロジェクトの設定情報。JSON形式で指定します。"
137
- "JSONの構造については https://annofab.com/docs/api/#operation/putProject のリクエストボディを参照してください。\n"
138
- "``file://`` を先頭に付けると、JSON形式のファイルを指定できます。",
139
- )
140
-
21
+ # create_project.py のparse_argsと同じ実装を使用
22
+ create_project.parse_args(parser)
23
+ # main関数のみ差し替え
141
24
  parser.set_defaults(subcommand_func=main)
142
25
 
143
26
 
144
27
  def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
145
28
  subcommand_name = "put"
146
- subcommand_help = "プロジェクトを作成します。"
29
+ subcommand_help = "[DEPRECATED] プロジェクトを作成します。"
30
+ subcommand_description = subcommand_help + "\n`project put` コマンドは非推奨です。代わりに 'project create'コマンドを使用してください。`project put` コマンドは2026年01月01日以降に廃止予定です。"
31
+
147
32
  epilog = "組織管理者、組織オーナを持つユーザで実行してください。"
148
33
 
149
- parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, epilog=epilog)
34
+ parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description=subcommand_description, epilog=epilog)
150
35
  parse_args(parser)
151
36
  return parser
@@ -4,10 +4,12 @@ from typing import Optional
4
4
  import annofabcli.project.change_organization_of_project
5
5
  import annofabcli.project.change_project_status
6
6
  import annofabcli.project.copy_project
7
+ import annofabcli.project.create_project
7
8
  import annofabcli.project.diff_projects
8
9
  import annofabcli.project.list_project
9
10
  import annofabcli.project.put_project
10
11
  import annofabcli.project.update_configuration
12
+ import annofabcli.project.update_project
11
13
  from annofabcli.common.cli import add_parser as common_add_parser
12
14
 
13
15
 
@@ -18,10 +20,12 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
18
20
  annofabcli.project.change_organization_of_project.add_parser(subparsers)
19
21
  annofabcli.project.change_project_status.add_parser(subparsers)
20
22
  annofabcli.project.copy_project.add_parser(subparsers)
23
+ annofabcli.project.create_project.add_parser(subparsers)
21
24
  annofabcli.project.diff_projects.add_parser(subparsers)
22
25
  annofabcli.project.list_project.add_parser(subparsers)
23
26
  annofabcli.project.put_project.add_parser(subparsers)
24
27
  annofabcli.project.update_configuration.add_parser(subparsers)
28
+ annofabcli.project.update_project.add_parser(subparsers)
25
29
 
26
30
 
27
31
  def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
@@ -0,0 +1,298 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import copy
5
+ import enum
6
+ import logging
7
+ import multiprocessing
8
+ import sys
9
+ from enum import Enum
10
+ from functools import partial
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ import annofabapi
15
+ import pandas
16
+ from pydantic import BaseModel
17
+
18
+ import annofabcli
19
+ import annofabcli.common.cli
20
+ from annofabcli.common.cli import (
21
+ COMMAND_LINE_ERROR_STATUS_CODE,
22
+ PARALLELISM_CHOICES,
23
+ CommandLine,
24
+ CommandLineWithConfirm,
25
+ build_annofabapi_resource_and_login,
26
+ get_json_from_args,
27
+ )
28
+ from annofabcli.common.facade import AnnofabApiFacade
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ class UpdateResult(Enum):
34
+ """更新結果の種類"""
35
+
36
+ SUCCESS = enum.auto()
37
+ """更新に成功した"""
38
+ SKIPPED = enum.auto()
39
+ """更新を実行しなかった(存在しないproject_id、ユーザー拒否等)"""
40
+ FAILED = enum.auto()
41
+ """更新を試みたが例外で失敗"""
42
+
43
+
44
+ class UpdatedProject(BaseModel):
45
+ """
46
+ 更新されるプロジェクト
47
+ """
48
+
49
+ project_id: str
50
+ """更新対象のプロジェクトを表すID"""
51
+ title: Optional[str] = None
52
+ """変更後のプロジェクトタイトル(指定した場合のみ更新)"""
53
+ overview: Optional[str] = None
54
+ """変更後のプロジェクト概要(指定した場合のみ更新)"""
55
+
56
+
57
+ class UpdateProjectMain(CommandLineWithConfirm):
58
+ def __init__(self, service: annofabapi.Resource, *, all_yes: bool = False) -> None:
59
+ self.service = service
60
+ CommandLineWithConfirm.__init__(self, all_yes)
61
+
62
+ def update_project(
63
+ self,
64
+ project_id: str,
65
+ *,
66
+ new_title: Optional[str] = None,
67
+ new_overview: Optional[str] = None,
68
+ project_index: Optional[int] = None,
69
+ ) -> UpdateResult:
70
+ """
71
+ 1個のプロジェクトを更新します。
72
+ """
73
+ # ログメッセージの先頭の変数
74
+ log_prefix = f"project_id='{project_id}' :: "
75
+ if project_index is not None:
76
+ log_prefix = f"{project_index + 1}件目 :: {log_prefix}"
77
+
78
+ old_project = self.service.wrapper.get_project_or_none(project_id)
79
+ if old_project is None:
80
+ logger.warning(f"{log_prefix}プロジェクトは存在しません。")
81
+ return UpdateResult.SKIPPED
82
+
83
+ # 更新する内容の確認メッセージを作成
84
+ changes = []
85
+ if new_title is not None:
86
+ changes.append(f"title='{old_project['title']}'を'{new_title}'に変更")
87
+ if new_overview is not None:
88
+ changes.append(f"overview='{old_project['overview']}'を'{new_overview}'に変更")
89
+
90
+ if len(changes) == 0:
91
+ logger.warning(f"{log_prefix}更新する内容が指定されていません。")
92
+ return UpdateResult.SKIPPED
93
+
94
+ change_message = "、".join(changes)
95
+ if not self.confirm_processing(f"{log_prefix}{change_message}しますか?"):
96
+ return UpdateResult.SKIPPED
97
+
98
+ request_body = copy.deepcopy(old_project)
99
+ request_body["last_updated_datetime"] = old_project["updated_datetime"]
100
+ request_body["status"] = old_project["project_status"]
101
+
102
+ if new_title is not None:
103
+ request_body["title"] = new_title
104
+ if new_overview is not None:
105
+ request_body["overview"] = new_overview
106
+
107
+ self.service.api.put_project(project_id, request_body=request_body)
108
+ logger.debug(f"{log_prefix}プロジェクトを更新しました。 :: {change_message}")
109
+ return UpdateResult.SUCCESS
110
+
111
+ def update_project_list_sequentially(
112
+ self,
113
+ updated_project_list: list[UpdatedProject],
114
+ ) -> None:
115
+ """複数のプロジェクトを逐次的に更新します。"""
116
+ success_count = 0
117
+ skipped_count = 0 # 更新を実行しなかった個数
118
+ failed_count = 0 # 更新に失敗した個数
119
+
120
+ logger.info(f"{len(updated_project_list)} 件のプロジェクトを更新します。")
121
+
122
+ for project_index, updated_project in enumerate(updated_project_list):
123
+ current_num = project_index + 1
124
+
125
+ # 進捗ログ出力
126
+ if current_num % 100 == 0:
127
+ logger.info(f"{current_num} / {len(updated_project_list)} 件目のプロジェクトを処理中...")
128
+
129
+ try:
130
+ result = self.update_project(
131
+ updated_project.project_id,
132
+ new_title=updated_project.title,
133
+ new_overview=updated_project.overview,
134
+ project_index=project_index,
135
+ )
136
+ if result == UpdateResult.SUCCESS:
137
+ success_count += 1
138
+ elif result == UpdateResult.SKIPPED:
139
+ skipped_count += 1
140
+ except Exception:
141
+ logger.warning(f"{current_num}件目 :: project_id='{updated_project.project_id}'のプロジェクトを更新するのに失敗しました。", exc_info=True)
142
+ failed_count += 1
143
+ continue
144
+
145
+ logger.info(f"{success_count} / {len(updated_project_list)} 件のプロジェクトを更新しました。(成功: {success_count}件, スキップ: {skipped_count}件, 失敗: {failed_count}件)")
146
+
147
+ def _update_project_wrapper(self, args: tuple[int, UpdatedProject]) -> UpdateResult:
148
+ index, updated_project = args
149
+ try:
150
+ return self.update_project(
151
+ project_id=updated_project.project_id,
152
+ new_title=updated_project.title,
153
+ new_overview=updated_project.overview,
154
+ project_index=index,
155
+ )
156
+ except Exception:
157
+ logger.warning(f"{index + 1}件目 :: project_id='{updated_project.project_id}'のプロジェクトを更新するのに失敗しました。", exc_info=True)
158
+ return UpdateResult.FAILED
159
+
160
+ def update_project_list_in_parallel(
161
+ self,
162
+ updated_project_list: list[UpdatedProject],
163
+ parallelism: int,
164
+ ) -> None:
165
+ """複数のプロジェクトを並列的に更新します。"""
166
+
167
+ logger.info(f"{len(updated_project_list)} 件のプロジェクトを更新します。{parallelism}個のプロセスを使用して並列実行します。")
168
+
169
+ partial_func = partial(self._update_project_wrapper)
170
+ with multiprocessing.Pool(parallelism) as pool:
171
+ result_list = pool.map(partial_func, enumerate(updated_project_list))
172
+ success_count = len([e for e in result_list if e == UpdateResult.SUCCESS])
173
+ skipped_count = len([e for e in result_list if e == UpdateResult.SKIPPED])
174
+ failed_count = len([e for e in result_list if e == UpdateResult.FAILED])
175
+
176
+ logger.info(f"{success_count} / {len(updated_project_list)} 件のプロジェクトを更新しました。(成功: {success_count}件, スキップ: {skipped_count}件, 失敗: {failed_count}件)")
177
+
178
+
179
+ def create_updated_project_list_from_dict(project_dict_list: list[dict[str, str]]) -> list[UpdatedProject]:
180
+ return [UpdatedProject.model_validate(e) for e in project_dict_list]
181
+
182
+
183
+ def create_updated_project_list_from_csv(csv_file: Path) -> list[UpdatedProject]:
184
+ """プロジェクトの情報が記載されているCSVを読み込み、UpdatedProjectのlistを返します。
185
+ CSVには以下の列が存在します。
186
+ * project_id (必須)
187
+ * title (任意)
188
+ * overview (任意)
189
+
190
+ Args:
191
+ csv_file (Path): CSVファイルのパス
192
+
193
+ Returns:
194
+ 更新対象のプロジェクトのlist
195
+ """
196
+ df_project = pandas.read_csv(
197
+ csv_file,
198
+ # 文字列として読み込むようにする
199
+ dtype={"project_id": "string", "title": "string", "overview": "string"},
200
+ )
201
+
202
+ project_dict_list = df_project.to_dict("records")
203
+ return [UpdatedProject.model_validate(e) for e in project_dict_list]
204
+
205
+
206
+ CLI_COMMON_MESSAGE = "annofabcli project update: error:"
207
+
208
+
209
+ class UpdateProject(CommandLine):
210
+ @staticmethod
211
+ def validate(args: argparse.Namespace) -> bool:
212
+ if args.parallelism is not None and not args.yes:
213
+ print( # noqa: T201
214
+ f"{CLI_COMMON_MESSAGE} argument --parallelism: '--parallelism'を指定するときは、'--yes' も指定する必要があります。",
215
+ file=sys.stderr,
216
+ )
217
+ return False
218
+
219
+ return True
220
+
221
+ def main(self) -> None:
222
+ args = self.args
223
+ if not self.validate(args):
224
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
225
+
226
+ main_obj = UpdateProjectMain(self.service, all_yes=self.all_yes)
227
+
228
+ if args.csv is not None:
229
+ updated_project_list = create_updated_project_list_from_csv(args.csv)
230
+
231
+ elif args.json is not None:
232
+ project_dict_list = get_json_from_args(args.json)
233
+ if not isinstance(project_dict_list, list):
234
+ print(f"{CLI_COMMON_MESSAGE} JSON形式が不正です。オブジェクトの配列を指定してください。", file=sys.stderr) # noqa: T201
235
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
236
+ updated_project_list = create_updated_project_list_from_dict(project_dict_list)
237
+ else:
238
+ raise RuntimeError("argparse により相互排他が保証されているため、ここには到達しません")
239
+
240
+ if args.parallelism is not None:
241
+ main_obj.update_project_list_in_parallel(updated_project_list=updated_project_list, parallelism=args.parallelism)
242
+ else:
243
+ main_obj.update_project_list_sequentially(updated_project_list=updated_project_list)
244
+
245
+
246
+ def main(args: argparse.Namespace) -> None:
247
+ service = build_annofabapi_resource_and_login(args)
248
+ facade = AnnofabApiFacade(service)
249
+ UpdateProject(service, facade, args).main()
250
+
251
+
252
+ def parse_args(parser: argparse.ArgumentParser) -> None:
253
+ file_group = parser.add_mutually_exclusive_group(required=True)
254
+ file_group.add_argument(
255
+ "--csv",
256
+ type=Path,
257
+ help=(
258
+ "更新対象のプロジェクトと更新後の値が記載されたCSVファイルのパスを指定します。\n"
259
+ "CSVのフォーマットは以下の通りです。"
260
+ "\n"
261
+ " * ヘッダ行あり, カンマ区切り\n"
262
+ " * project_id (required)\n"
263
+ " * title (optional)\n"
264
+ " * overview (optional)\n"
265
+ "更新しないプロパティは、セルの値を空欄にしてください。\n"
266
+ ),
267
+ )
268
+
269
+ JSON_SAMPLE = '[{"project_id":"prj1","title":"new_title1"},{"project_id":"prj2","overview":"new_overview2"}]' # noqa: N806
270
+ file_group.add_argument(
271
+ "--json",
272
+ type=str,
273
+ help=(
274
+ "更新対象のプロジェクトと更新後の値をJSON形式で指定します。\n"
275
+ "JSONの各キーは ``--csv`` に渡すCSVの各列に対応しています。\n"
276
+ "``file://`` を先頭に付けるとjsonファイルを指定できます。\n"
277
+ f"(ex) ``{JSON_SAMPLE}`` \n"
278
+ "更新しないプロパティは、キーを記載しないか値をnullにしてください。\n"
279
+ ),
280
+ )
281
+
282
+ parser.add_argument(
283
+ "--parallelism",
284
+ type=int,
285
+ choices=PARALLELISM_CHOICES,
286
+ help="使用するプロセス数(並列度)。指定しない場合は、逐次的に処理します。指定する場合は ``--yes`` も一緒に指定する必要があります。",
287
+ )
288
+
289
+ parser.set_defaults(subcommand_func=main)
290
+
291
+
292
+ def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
293
+ subcommand_name = "update"
294
+ subcommand_help = "プロジェクトのタイトルまたは概要を更新します。"
295
+ epilog = "プロジェクトオーナロールを持つユーザで実行してください。"
296
+ parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, epilog=epilog)
297
+ parse_args(parser)
298
+ return parser
@@ -230,10 +230,9 @@ class ListAnnotationArea(CommandLine):
230
230
 
231
231
  def download_and_print_annotation_area(project_id: str, temp_dir: Path, *, is_latest: bool, annotation_path: Optional[Path]) -> None:
232
232
  if annotation_path is None:
233
- annotation_path = temp_dir / f"{project_id}__annotation.zip"
234
- downloading_obj.download_annotation_zip(
233
+ annotation_path = downloading_obj.download_annotation_zip_to_dir(
235
234
  project_id,
236
- dest_path=annotation_path,
235
+ temp_dir,
237
236
  is_latest=is_latest,
238
237
  )
239
238
  print_annotation_area(
@@ -571,17 +571,15 @@ class ListAnnotationAttributeFilledCount(CommandLine):
571
571
 
572
572
  downloading_obj = DownloadingFile(self.service)
573
573
 
574
- # `NamedTemporaryFile`を使わない理由: Windowsで`PermissionError`が発生するため
575
- # https://qiita.com/yuji38kwmt/items/c6f50e1fc03dafdcdda0 参考
576
- with tempfile.TemporaryDirectory() as str_temp_dir:
574
+ def download_and_process_annotation(temp_dir: Path, *, is_latest: bool, annotation_path: Optional[Path]) -> None:
577
575
  # タスク全件ファイルは、フレーム番号を参照するのに利用する
578
576
  if project_id is not None and group_by == GroupBy.INPUT_DATA_ID:
579
577
  # group_byで条件を絞り込んでいる理由:
580
578
  # タスクIDで集計する際は、フレーム番号は出力しないので、タスク全件ファイルをダウンロードする必要はないため
581
- task_json_path = Path(str_temp_dir) / f"{project_id}__task.json"
582
- downloading_obj.download_task_json(
579
+ task_json_path = downloading_obj.download_task_json_to_dir(
583
580
  project_id,
584
- dest_path=str(task_json_path),
581
+ temp_dir,
582
+ is_latest=is_latest,
585
583
  )
586
584
  else:
587
585
  task_json_path = None
@@ -600,16 +598,37 @@ class ListAnnotationAttributeFilledCount(CommandLine):
600
598
 
601
599
  if annotation_path is None:
602
600
  assert project_id is not None
603
- annotation_path = Path(str_temp_dir) / f"{project_id}__annotation.zip"
604
- downloading_obj.download_annotation_zip(
601
+ annotation_path = downloading_obj.download_annotation_zip_to_dir(
605
602
  project_id,
606
- dest_path=str(annotation_path),
607
- is_latest=args.latest,
603
+ temp_dir,
604
+ is_latest=is_latest,
608
605
  )
609
606
  func(annotation_path=annotation_path)
610
607
  else:
611
608
  func(annotation_path=annotation_path)
612
609
 
610
+ if project_id is not None:
611
+ if args.temp_dir is not None:
612
+ download_and_process_annotation(temp_dir=args.temp_dir, is_latest=args.latest, annotation_path=annotation_path)
613
+ else:
614
+ with tempfile.TemporaryDirectory() as str_temp_dir:
615
+ download_and_process_annotation(temp_dir=Path(str_temp_dir), is_latest=args.latest, annotation_path=annotation_path)
616
+ else:
617
+ # プロジェクトIDが指定されていない場合は、アノテーションパスが必須なので、一時ディレクトリは不要
618
+ assert annotation_path is not None
619
+ func = partial(
620
+ main_obj.print_annotation_count,
621
+ project_id=project_id,
622
+ task_json_path=None,
623
+ group_by=group_by,
624
+ output_format=output_format,
625
+ output_file=output_file,
626
+ target_task_ids=task_id_list,
627
+ task_query=task_query,
628
+ include_flag_attribute=args.include_flag_attribute,
629
+ )
630
+ func(annotation_path=annotation_path)
631
+
613
632
 
614
633
  def main(args: argparse.Namespace) -> None:
615
634
  service = build_annofabapi_resource_and_login(args)
@@ -671,6 +690,12 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
671
690
  help="``--annotation`` を指定しないとき、最新のアノテーションzipを参照します。このオプションを指定すると、アノテーションzipを更新するのに数分待ちます。",
672
691
  )
673
692
 
693
+ parser.add_argument(
694
+ "--temp_dir",
695
+ type=Path,
696
+ help="指定したディレクトリに、アノテーションZIPなどの一時ファイルをダウンロードします。",
697
+ )
698
+
674
699
  parser.set_defaults(subcommand_func=main)
675
700
 
676
701