annofabcli 1.95.0__py3-none-any.whl → 1.96.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/__version__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.95.0" # `poetry-dynamic-versioning`を使ってGitHubのバージョンタグを取得している。変更不要
1
+ __version__ = "1.96.1" # `poetry-dynamic-versioning`を使ってGitHubのバージョンタグを取得している。変更不要
@@ -20,15 +20,20 @@ from annofabcli.common.utils import print_according_to_format
20
20
  logger = logging.getLogger(__name__)
21
21
 
22
22
 
23
- class ListAttributeRestriction(CommandLine):
23
+ class ExportAnnotationSpecs(CommandLine):
24
24
  COMMON_MESSAGE = "annofabcli annotation_specs export: error:"
25
25
 
26
26
  def get_history_id_from_before_index(self, project_id: str, before: int) -> Optional[str]:
27
27
  histories, _ = self.service.api.get_annotation_specs_histories(project_id)
28
- if before + 1 > len(histories):
29
- logger.warning(f"アノテーション仕様の履歴は{len(histories)}個のため、最新より{before}個前のアノテーション仕様は見つかりませんでした。")
28
+ sorted_histories = sorted(histories, key=lambda x: x["updated_datetime"], reverse=True)
29
+
30
+ if before + 1 > len(sorted_histories):
31
+ logger.warning(
32
+ f"アノテーション仕様の履歴は{len(sorted_histories)}個のため、最新より{before}個前のアノテーション仕様は見つかりませんでした。"
33
+ )
30
34
  return None
31
- history = histories[-(before + 1)]
35
+
36
+ history = sorted_histories[before]
32
37
  return history["history_id"]
33
38
 
34
39
  def get_exported_annotation_specs(self, project_id: str, history_id: Optional[str]) -> dict[str, Any]:
@@ -107,7 +112,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
107
112
  def main(args: argparse.Namespace) -> None:
108
113
  service = build_annofabapi_resource_and_login(args)
109
114
  facade = AnnofabApiFacade(service)
110
- ListAttributeRestriction(service, facade, args).main()
115
+ ExportAnnotationSpecs(service, facade, args).main()
111
116
 
112
117
 
113
118
  def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
@@ -1,7 +1,7 @@
1
1
  import argparse
2
2
  import logging
3
+ import re
3
4
  import sys
4
- import uuid
5
5
  from dataclasses import dataclass
6
6
  from functools import partial
7
7
  from multiprocessing import Pool
@@ -25,14 +25,11 @@ from annofabcli.common.cli import (
25
25
  get_json_from_args,
26
26
  prompt_yesnoall,
27
27
  )
28
- from annofabcli.common.dataclasses import WaitOptions
29
28
  from annofabcli.common.facade import AnnofabApiFacade
30
29
  from annofabcli.common.utils import get_file_scheme_path
31
30
 
32
31
  logger = logging.getLogger(__name__)
33
32
 
34
- DEFAULT_WAIT_OPTIONS = WaitOptions(interval=60, max_tries=360)
35
-
36
33
 
37
34
  @dataclass
38
35
  class CsvInputData(DataClassJsonMixin):
@@ -43,7 +40,6 @@ class CsvInputData(DataClassJsonMixin):
43
40
  input_data_name: str
44
41
  input_data_path: str
45
42
  input_data_id: Optional[str] = None
46
- sign_required: Optional[bool] = None
47
43
 
48
44
 
49
45
  @dataclass
@@ -55,7 +51,14 @@ class InputDataForPut(DataClassJsonMixin):
55
51
  input_data_name: str
56
52
  input_data_path: str
57
53
  input_data_id: str
58
- sign_required: Optional[bool]
54
+
55
+
56
+ def convert_input_data_name_to_input_data_id(input_data_name: str) -> str:
57
+ """
58
+ 入力データ名から、入力データIDを生成します。
59
+ * IDに使えない文字以外は`__`に変換する。
60
+ """
61
+ return re.sub(r"[^a-zA-Z0-9_.-]", "__", input_data_name)
59
62
 
60
63
 
61
64
  def read_input_data_csv(csv_file: Path) -> pandas.DataFrame:
@@ -64,7 +67,6 @@ def read_input_data_csv(csv_file: Path) -> pandas.DataFrame:
64
67
  * input_data_name
65
68
  * input_data_path
66
69
  * input_data_id
67
- * sign_required
68
70
 
69
71
  Args:
70
72
  csv_file (Path): CSVファイルのパス
@@ -76,6 +78,12 @@ def read_input_data_csv(csv_file: Path) -> pandas.DataFrame:
76
78
  str(csv_file),
77
79
  sep=",",
78
80
  header=None,
81
+ # names引数に"sign_required"を指定している理由:
82
+ # v1.96.0以前では、`sign_required`列を読み込んでいた。したがって、4列のCSVを使っているユーザーは存在する
83
+ # CSVの列数が`names`引数で指定した列数より大きい場合、右側から列名が割り当てられる。
84
+ # (https://qiita.com/yuji38kwmt/items/ac46c3d0ccac109410ba)
85
+ # したがって、"sign_required"がないと4列のCSVは読み込めない。
86
+ # 4列のCSVもしばらくサポートするため、"sign_required"を指定している。
79
87
  names=("input_data_name", "input_data_path", "input_data_id", "sign_required"),
80
88
  # IDと名前は必ず文字列として読み込むようにする
81
89
  dtype={"input_data_id": str, "input_data_name": str},
@@ -126,8 +134,8 @@ class SubPutInputData:
126
134
 
127
135
  file_path = get_file_scheme_path(csv_input_data.input_data_path)
128
136
  if file_path is not None:
129
- request_body.update({"input_data_name": csv_input_data.input_data_name, "sign_required": csv_input_data.sign_required})
130
- logger.debug(f"'{file_path}'を入力データとして登録します。input_data_name={csv_input_data.input_data_name}")
137
+ request_body.update({"input_data_name": csv_input_data.input_data_name})
138
+ logger.debug(f"'{file_path}'を入力データとして登録します。input_data_name='{csv_input_data.input_data_name}'")
131
139
  self.service.wrapper.put_input_data_from_file(
132
140
  project_id, input_data_id=csv_input_data.input_data_id, file_path=file_path, request_body=request_body
133
141
  )
@@ -137,7 +145,6 @@ class SubPutInputData:
137
145
  {
138
146
  "input_data_name": csv_input_data.input_data_name,
139
147
  "input_data_path": csv_input_data.input_data_path,
140
- "sign_required": csv_input_data.sign_required,
141
148
  }
142
149
  )
143
150
 
@@ -181,12 +188,13 @@ class SubPutInputData:
181
188
 
182
189
  return self.confirm_processing(message_for_confirm)
183
190
 
184
- def put_input_data_main(self, project_id: str, csv_input_data: CsvInputData, overwrite: bool = False) -> bool: # noqa: FBT001, FBT002
191
+ def put_input_data_main(self, project_id: str, csv_input_data: CsvInputData, *, overwrite: bool = False) -> bool:
185
192
  input_data = InputDataForPut(
186
193
  input_data_name=csv_input_data.input_data_name,
187
194
  input_data_path=csv_input_data.input_data_path,
188
- input_data_id=csv_input_data.input_data_id if csv_input_data.input_data_id is not None else str(uuid.uuid4()),
189
- sign_required=csv_input_data.sign_required,
195
+ input_data_id=csv_input_data.input_data_id
196
+ if csv_input_data.input_data_id is not None
197
+ else convert_input_data_name_to_input_data_id(csv_input_data.input_data_name),
190
198
  )
191
199
 
192
200
  last_updated_datetime = None
@@ -194,10 +202,10 @@ class SubPutInputData:
194
202
 
195
203
  if dict_input_data is not None:
196
204
  if overwrite:
197
- logger.debug(f"input_data_id={input_data.input_data_id} はすでに存在します。")
205
+ logger.debug(f"input_data_id='{input_data.input_data_id}' はすでに存在します。")
198
206
  last_updated_datetime = dict_input_data["updated_datetime"]
199
207
  else:
200
- logger.debug(f"input_data_id={input_data.input_data_id} がすでに存在するのでスキップします。")
208
+ logger.debug(f"input_data_id='{input_data.input_data_id}' がすでに存在するのでスキップします。")
201
209
  return False
202
210
 
203
211
  file_path = get_file_scheme_path(input_data.input_data_path)
@@ -278,14 +286,12 @@ class PutInputData(CommandLine):
278
286
 
279
287
  @staticmethod
280
288
  def get_input_data_list_from_df(df: pandas.DataFrame) -> list[CsvInputData]:
281
- def create_input_data(e: Any): # noqa: ANN202, ANN401
289
+ def create_input_data(e: Any) -> CsvInputData: # noqa: ANN401
282
290
  input_data_id = e.input_data_id if not pandas.isna(e.input_data_id) else None
283
- sign_required: Optional[bool] = e.sign_required if pandas.notna(e.sign_required) else None
284
291
  return CsvInputData(
285
292
  input_data_name=e.input_data_name,
286
293
  input_data_path=e.input_data_path,
287
294
  input_data_id=input_data_id,
288
- sign_required=sign_required,
289
295
  )
290
296
 
291
297
  input_data_list = [create_input_data(e) for e in df.itertuples()]
@@ -381,7 +387,6 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
381
387
  " * 1列目: input_data_name (required)\n"
382
388
  " * 2列目: input_data_path (required)\n"
383
389
  " * 3列目: input_data_id\n"
384
- " * 4列目: sign_required (bool)\n"
385
390
  ),
386
391
  )
387
392
 
@@ -400,25 +405,20 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
400
405
  parser.add_argument(
401
406
  "--overwrite",
402
407
  action="store_true",
403
- help="指定した場合、input_data_idがすでに存在していたら上書きします。指定しなければ、スキップします。"
404
- " ``--csv`` , ``--json`` を指定したときのみ有効なオプションです。",
408
+ help="指定した場合、input_data_idがすでに存在していたら上書きします。指定しなければ、スキップします。",
405
409
  )
406
410
 
407
411
  parser.add_argument(
408
412
  "--allow_duplicated_input_data",
409
413
  action="store_true",
410
- help=(
411
- "``--csv`` , ``--json`` に渡した入力データの重複(input_data_name, input_data_path)を許可します。\n"
412
- "``--csv`` , ``--json`` を指定したときのみ有効なオプションです。"
413
- ),
414
+ help=("``--csv`` , ``--json`` に渡した入力データの重複(input_data_name, input_data_path)を許可します。\n"),
414
415
  )
415
416
 
416
417
  parser.add_argument(
417
418
  "--parallelism",
418
419
  type=int,
419
420
  choices=PARALLELISM_CHOICES,
420
- help="並列度。指定しない場合は、逐次的に処理します。"
421
- "``--csv`` , ``--json`` を指定したときのみ有効なオプションです。また、必ず ``--yes`` を指定してください。",
421
+ help="並列度。指定しない場合は、逐次的に処理します。指定する場合は、 ``--yes`` も指定してください。",
422
422
  )
423
423
 
424
424
  parser.set_defaults(subcommand_func=main)
@@ -427,9 +427,8 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
427
427
  def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
428
428
  subcommand_name = "put"
429
429
  subcommand_help = "入力データを登録します。"
430
- description = "CSVに記載された情報から、入力データを登録します。"
431
430
  epilog = "オーナロールを持つユーザで実行してください。"
432
431
 
433
- parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog)
432
+ parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, epilog=epilog)
434
433
  parse_args(parser)
435
434
  return parser
@@ -0,0 +1,310 @@
1
+ # pylint: disable=too-many-lines
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import logging
6
+ import sys
7
+ import tempfile
8
+ import zipfile
9
+ from collections.abc import Collection, Iterator
10
+ from pathlib import Path
11
+ from typing import Any, Literal, Optional, Union
12
+
13
+ import pandas
14
+ import pydantic
15
+ from annofabapi.models import ProjectMemberRole
16
+ from annofabapi.parser import (
17
+ SimpleAnnotationParser,
18
+ lazy_parse_simple_annotation_dir,
19
+ lazy_parse_simple_annotation_zip,
20
+ )
21
+
22
+ import annofabcli
23
+ import annofabcli.common.cli
24
+ from annofabcli.common.cli import (
25
+ COMMAND_LINE_ERROR_STATUS_CODE,
26
+ ArgumentParser,
27
+ CommandLine,
28
+ build_annofabapi_resource_and_login,
29
+ )
30
+ from annofabcli.common.download import DownloadingFile
31
+ from annofabcli.common.enums import FormatArgument
32
+ from annofabcli.common.facade import (
33
+ AnnofabApiFacade,
34
+ TaskQuery,
35
+ match_annotation_with_task_query,
36
+ )
37
+ from annofabcli.common.type_util import assert_noreturn
38
+ from annofabcli.common.utils import print_csv, print_json
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+
43
+ def lazy_parse_simple_annotation_by_input_data(annotation_path: Path) -> Iterator[SimpleAnnotationParser]:
44
+ if not annotation_path.exists():
45
+ raise RuntimeError(f"'{annotation_path}' は存在しません。")
46
+
47
+ if annotation_path.is_dir():
48
+ return lazy_parse_simple_annotation_dir(annotation_path)
49
+ elif zipfile.is_zipfile(str(annotation_path)):
50
+ return lazy_parse_simple_annotation_zip(annotation_path)
51
+ else:
52
+ raise RuntimeError(f"'{annotation_path}'は、zipファイルまたはディレクトリではありません。")
53
+
54
+
55
+ class AnnotationAttribute(pydantic.BaseModel):
56
+ """
57
+ 入力データまたはタスク単位の区間アノテーションの長さ情報。
58
+ """
59
+
60
+ task_id: str
61
+ task_status: str
62
+ task_phase: str
63
+ task_phase_stage: int
64
+
65
+ input_data_id: str
66
+ input_data_name: str
67
+ annotation_id: str
68
+ label: str
69
+ attributes: dict[str, Union[str, int, bool]]
70
+
71
+
72
+ def get_annotation_attribute_list_from_annotation_json(
73
+ simple_annotation: dict[str, Any], *, target_labels: Collection[str] | None = None
74
+ ) -> list[AnnotationAttribute]:
75
+ """
76
+ 1個のアノテーションJSONに対して、アノテーションの属性情報を取得します。
77
+
78
+ Args:
79
+ simple_annotation: アノテーションJSONファイルの内容
80
+ target_labels: 絞り込むラベルのcollection
81
+ """
82
+ details = simple_annotation["details"]
83
+
84
+ result = []
85
+ for detail in details:
86
+ if target_labels is not None: # noqa: SIM102
87
+ if detail["label"] not in target_labels:
88
+ continue
89
+
90
+ result.append(
91
+ AnnotationAttribute(
92
+ task_id=simple_annotation["task_id"],
93
+ task_status=simple_annotation["task_status"],
94
+ task_phase=simple_annotation["task_phase"],
95
+ task_phase_stage=simple_annotation["task_phase_stage"],
96
+ input_data_id=simple_annotation["input_data_id"],
97
+ input_data_name=simple_annotation["input_data_name"],
98
+ label=detail["label"],
99
+ annotation_id=detail["annotation_id"],
100
+ attributes=detail["attributes"],
101
+ )
102
+ )
103
+ return result
104
+
105
+
106
+ def get_annotation_attribute_list_from_annotation_zipdir_path(
107
+ annotation_zipdir_path: Path,
108
+ *,
109
+ target_task_ids: Optional[Collection[str]] = None,
110
+ task_query: Optional[TaskQuery] = None,
111
+ target_labels: Collection[str] | None = None,
112
+ ) -> list[AnnotationAttribute]:
113
+ """
114
+ アノテーションzipまたはそれを展開したディレクトリから、アノテーションの属性のlistを取得します。
115
+
116
+ Args:
117
+ """
118
+ target_task_ids = set(target_task_ids) if target_task_ids is not None else None
119
+
120
+ iter_parser = lazy_parse_simple_annotation_by_input_data(annotation_zipdir_path)
121
+
122
+ result = []
123
+ logger.debug("アノテーションzipまたはディレクトリを読み込み中")
124
+ for index, parser in enumerate(iter_parser):
125
+ if (index + 1) % 1000 == 0:
126
+ logger.debug(f"{index + 1} 件目のJSONを読み込み中")
127
+
128
+ if target_task_ids is not None and parser.task_id not in target_task_ids:
129
+ continue
130
+
131
+ simple_annotation_dict = parser.load_json()
132
+ if task_query is not None: # noqa: SIM102
133
+ if not match_annotation_with_task_query(simple_annotation_dict, task_query):
134
+ continue
135
+
136
+ sub_result = get_annotation_attribute_list_from_annotation_json(simple_annotation_dict, target_labels=target_labels)
137
+ result.extend(sub_result)
138
+
139
+ return result
140
+
141
+
142
+ def print_annotation_attribute_list_as_csv(annotation_attribute_list: list, output_file: Optional[Path]) -> None:
143
+ df = pandas.json_normalize(annotation_attribute_list)
144
+
145
+ base_columns = [
146
+ "task_id",
147
+ "task_status",
148
+ "task_phase",
149
+ "task_phase_stage",
150
+ "input_data_id",
151
+ "input_data_name",
152
+ "annotation_id",
153
+ "label",
154
+ ]
155
+ attribute_columns = [col for col in df.columns if col.startswith("attributes.")]
156
+ columns = base_columns + attribute_columns
157
+ print_csv(df[columns], output_file)
158
+
159
+
160
+ def print_annotation_attribute_list(
161
+ annotation_attribute_list: list[AnnotationAttribute],
162
+ output_file: Path,
163
+ output_format: Literal[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON],
164
+ ) -> None:
165
+ tmp_annotation_attribute_list = [e.model_dump() for e in annotation_attribute_list]
166
+ if output_format == FormatArgument.CSV:
167
+ print_annotation_attribute_list_as_csv(tmp_annotation_attribute_list, output_file)
168
+ elif output_format == FormatArgument.JSON:
169
+ print_json(tmp_annotation_attribute_list, output=output_file, is_pretty=False)
170
+ elif output_format == FormatArgument.PRETTY_JSON:
171
+ print_json(tmp_annotation_attribute_list, output=output_file, is_pretty=True)
172
+ else:
173
+ raise assert_noreturn(output_format)
174
+
175
+
176
+ class ListAnnotationAttribute(CommandLine):
177
+ COMMON_MESSAGE = "annofabcli statistics list_annotation_attribute: error:"
178
+
179
+ def validate(self, args: argparse.Namespace) -> bool:
180
+ if args.project_id is None and args.annotation is None:
181
+ print( # noqa: T201
182
+ f"{self.COMMON_MESSAGE} argument --project_id: '--annotation'が未指定のときは、'--project_id' を指定してください。",
183
+ file=sys.stderr,
184
+ )
185
+ return False
186
+
187
+ return True
188
+
189
+ def main(self) -> None:
190
+ args = self.args
191
+
192
+ if not self.validate(args):
193
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
194
+
195
+ project_id: Optional[str] = args.project_id
196
+ if project_id is not None:
197
+ super().validate_project(project_id, project_member_roles=[ProjectMemberRole.OWNER, ProjectMemberRole.TRAINING_DATA_USER])
198
+
199
+ annotation_path = Path(args.annotation) if args.annotation is not None else None
200
+
201
+ task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id) if args.task_id is not None else None
202
+ label_name_list = annofabcli.common.cli.get_list_from_args(args.label_name) if args.label_name is not None else None
203
+ task_query = TaskQuery.from_dict(annofabcli.common.cli.get_json_from_args(args.task_query)) if args.task_query is not None else None
204
+
205
+ output_file: Path = args.output
206
+ output_format = FormatArgument(args.format)
207
+
208
+ downloading_obj = DownloadingFile(self.service)
209
+
210
+ def download_and_print_annotation_attribute_list(
211
+ project_id: str, temp_dir: Path, *, is_latest: bool, annotation_path: Optional[Path]
212
+ ) -> None:
213
+ if annotation_path is None:
214
+ annotation_path = temp_dir / f"{project_id}__annotation.zip"
215
+ downloading_obj.download_annotation_zip(
216
+ project_id,
217
+ dest_path=annotation_path,
218
+ is_latest=is_latest,
219
+ )
220
+
221
+ annotation_attribute_list = get_annotation_attribute_list_from_annotation_zipdir_path(
222
+ annotation_zipdir_path=annotation_path, target_task_ids=task_id_list, task_query=task_query, target_labels=label_name_list
223
+ )
224
+ print_annotation_attribute_list(annotation_attribute_list, output_file, output_format) # type: ignore[arg-type]
225
+
226
+ if project_id is not None:
227
+ if args.temp_dir is not None:
228
+ download_and_print_annotation_attribute_list(
229
+ project_id=project_id, temp_dir=args.temp_dir, is_latest=args.latest, annotation_path=annotation_path
230
+ )
231
+ else:
232
+ # `NamedTemporaryFile`を使わない理由: Windowsで`PermissionError`が発生するため
233
+ # https://qiita.com/yuji38kwmt/items/c6f50e1fc03dafdcdda0 参考
234
+ with tempfile.TemporaryDirectory() as str_temp_dir:
235
+ download_and_print_annotation_attribute_list(
236
+ project_id=project_id, temp_dir=Path(str_temp_dir), is_latest=args.latest, annotation_path=annotation_path
237
+ )
238
+ else:
239
+ assert annotation_path is not None
240
+ annotation_attribute_list = get_annotation_attribute_list_from_annotation_zipdir_path(
241
+ annotation_zipdir_path=annotation_path, target_task_ids=task_id_list, task_query=task_query, target_labels=label_name_list
242
+ )
243
+ print_annotation_attribute_list(annotation_attribute_list, output_file, output_format) # type: ignore[arg-type]
244
+
245
+
246
+ def parse_args(parser: argparse.ArgumentParser) -> None:
247
+ argument_parser = ArgumentParser(parser)
248
+
249
+ annotation_group = parser.add_mutually_exclusive_group(required=True)
250
+ annotation_group.add_argument(
251
+ "--annotation",
252
+ type=str,
253
+ help="アノテーションzip、またはzipを展開したディレクトリを指定します。指定しない場合はAnnofabからダウンロードします。",
254
+ )
255
+
256
+ annotation_group.add_argument("-p", "--project_id", type=str, help="対象プロジェクトの project_id")
257
+
258
+ argument_parser.add_format(
259
+ choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON],
260
+ default=FormatArgument.CSV,
261
+ )
262
+
263
+ argument_parser.add_output()
264
+
265
+ parser.add_argument(
266
+ "-tq",
267
+ "--task_query",
268
+ type=str,
269
+ help="集計対象タスクを絞り込むためのクエリ条件をJSON形式で指定します。使用できるキーは task_id, status, phase, phase_stage です。"
270
+ " ``file://`` を先頭に付けると、JSON形式のファイルを指定できます。",
271
+ )
272
+ argument_parser.add_task_id(required=False)
273
+
274
+ parser.add_argument(
275
+ "--label_name",
276
+ type=str,
277
+ nargs="+",
278
+ required=False,
279
+ help="出力対象のアノテーションのラベル名(英語)を指定します。指定しない場合はラベル名で絞り込みません。"
280
+ " ``file://`` を先頭に付けると、ラベル名の一覧が記載されたファイルを指定できます。",
281
+ )
282
+
283
+ parser.add_argument(
284
+ "--latest",
285
+ action="store_true",
286
+ help="``--annotation`` を指定しないとき、最新のアノテーションzipを参照します。このオプションを指定すると、アノテーションzipを更新するのに数分待ちます。", # noqa: E501
287
+ )
288
+
289
+ parser.add_argument(
290
+ "--temp_dir",
291
+ type=Path,
292
+ help="指定したディレクトリに、アノテーションZIPなどの一時ファイルをダウンロードします。",
293
+ )
294
+
295
+ parser.set_defaults(subcommand_func=main)
296
+
297
+
298
+ def main(args: argparse.Namespace) -> None:
299
+ service = build_annofabapi_resource_and_login(args)
300
+ facade = AnnofabApiFacade(service)
301
+ ListAnnotationAttribute(service, facade, args).main()
302
+
303
+
304
+ def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
305
+ subcommand_name = "list_annotation_attribute"
306
+ subcommand_help = "アノテーションZIPを読み込み、アノテーションの属性値の一覧を出力します。"
307
+ epilog = "オーナロールまたはアノテーションユーザロールを持つユーザで実行してください。"
308
+ parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description=subcommand_help, epilog=epilog)
309
+ parse_args(parser)
310
+ 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_attribute
7
8
  import annofabcli.statistics.list_annotation_count
8
9
  import annofabcli.statistics.list_annotation_duration
9
10
  import annofabcli.statistics.list_video_duration
@@ -21,6 +22,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
21
22
  subparsers = parser.add_subparsers(dest="subcommand_name")
22
23
 
23
24
  # サブコマンドの定義
25
+ annofabcli.statistics.list_annotation_attribute.add_parser(subparsers)
24
26
  annofabcli.statistics.list_annotation_count.add_parser(subparsers)
25
27
  annofabcli.statistics.list_annotation_duration.add_parser(subparsers)
26
28
  annofabcli.statistics.list_video_duration.add_parser(subparsers)
@@ -92,14 +92,14 @@ class DeleteSupplementaryDataMain(CommandLineWithConfirm):
92
92
  supplementary_data = _get_supplementary_data_list(supplementary_data_id)
93
93
  if supplementary_data is None:
94
94
  logger.warning(
95
- f"input_data_id={input_data_id} の入力データに、"
96
- f"supplementary_data_id={supplementary_data_id} の補助情報は存在しないのでスキップします。"
95
+ f"input_data_id='{input_data_id}' の入力データに、"
96
+ f"supplementary_data_id='{supplementary_data_id}' の補助情報は存在しないのでスキップします。"
97
97
  )
98
98
  continue
99
99
 
100
100
  message_for_confirm = (
101
- f"補助情報 supplementary_data_id={supplementary_data_id}, "
102
- f"supplementary_data_name={supplementary_data['supplementary_data_name']} を削除しますか?"
101
+ f"補助情報 supplementary_data_id='{supplementary_data_id}', "
102
+ f"supplementary_data_name='{supplementary_data['supplementary_data_name']}' を削除しますか?"
103
103
  )
104
104
  if not self.confirm_processing(message_for_confirm):
105
105
  continue
@@ -107,17 +107,17 @@ class DeleteSupplementaryDataMain(CommandLineWithConfirm):
107
107
  try:
108
108
  self.service.api.delete_supplementary_data(project_id, input_data_id=input_data_id, supplementary_data_id=supplementary_data_id)
109
109
  logger.debug(
110
- f"補助情報 supplementary_data_id={supplementary_data_id}, "
111
- f"supplementary_data_name={supplementary_data['supplementary_data_name']} を削除しました。"
112
- f"(入力データ input_data_id={input_data_id}, "
113
- f"input_data_name={input_data['input_data_name']} に紐付いている)"
110
+ f"補助情報 supplementary_data_id='{supplementary_data_id}', "
111
+ f"supplementary_data_name='{supplementary_data['supplementary_data_name']}' を削除しました。"
112
+ f"(入力データ input_data_id='{input_data_id}', "
113
+ f"input_data_name='{input_data['input_data_name']}' に紐付いている)"
114
114
  )
115
115
  deleted_count += 1
116
- except requests.HTTPError as e:
117
- logger.warning(e)
116
+ except requests.HTTPError:
118
117
  logger.warning(
119
- f"補助情報 supplementary_data_id={supplementary_data_id}, "
120
- f"supplementary_data_name={supplementary_data['supplementary_data_name']} の削除に失敗しました。"
118
+ f"補助情報 supplementary_data_id='{supplementary_data_id}', "
119
+ f"supplementary_data_name='{supplementary_data['supplementary_data_name']}' の削除に失敗しました。",
120
+ exc_info=True,
121
121
  )
122
122
  continue
123
123
  return deleted_count
@@ -128,9 +128,8 @@ class DeleteSupplementaryDataMain(CommandLineWithConfirm):
128
128
  for input_data_id, supplementary_data_id_list in input_data_dict.items():
129
129
  try:
130
130
  deleted_count += self.delete_supplementary_data_list_for_input_data(project_id, input_data_id, supplementary_data_id_list)
131
- except Exception as e: # pylint: disable=broad-except
132
- logger.warning(e)
133
- logger.warning(f"入力データ(input_data_id={input_data_id})配下の補助情報の削除に失敗しました。")
131
+ except Exception: # pylint: disable=broad-except
132
+ logger.warning(f"入力データ(input_data_id='{input_data_id}')配下の補助情報の削除に失敗しました。", exc_info=True)
134
133
 
135
134
  logger.info(f"{deleted_count} / {total_count} 件の補助情報を削除しました。")
136
135
 
@@ -155,15 +154,15 @@ class DeleteSupplementaryDataMain(CommandLineWithConfirm):
155
154
  try:
156
155
  self.service.api.delete_supplementary_data(project_id, input_data_id=input_data_id, supplementary_data_id=supplementary_data_id)
157
156
  logger.debug(
158
- f"補助情報を削除しました。input_data_id={input_data_id}, supplementary_data_id={supplementary_data_id}, "
157
+ f"補助情報を削除しました。input_data_id='{input_data_id}', supplementary_data_id='{supplementary_data_id}', "
159
158
  f"supplementary_data_name={supplementary_data['supplementary_data_name']}"
160
159
  )
161
160
  deleted_count += 1
162
- except requests.HTTPError as e:
163
- logger.warning(e)
161
+ except requests.HTTPError:
164
162
  logger.warning(
165
- f"補助情報の削除に失敗しました。input_data_id={input_data_id}, supplementary_data_id={supplementary_data_id}, "
166
- f"supplementary_data_name={supplementary_data['supplementary_data_name']}"
163
+ f"補助情報の削除に失敗しました。input_data_id='{input_data_id}', supplementary_data_id='{supplementary_data_id}', "
164
+ f"supplementary_data_name='{supplementary_data['supplementary_data_name']}'",
165
+ exc_info=True,
167
166
  )
168
167
  continue
169
168
 
@@ -174,7 +173,7 @@ class DeleteSupplementaryDataMain(CommandLineWithConfirm):
174
173
  for input_data_id in input_data_id_list:
175
174
  input_data = self.service.wrapper.get_input_data_or_none(project_id, input_data_id)
176
175
  if input_data is None:
177
- logger.warning(f"input_data_id={input_data_id} の入力データは存在しないので、補助情報の削除をスキップします。")
176
+ logger.warning(f"input_data_id='{input_data_id}' の入力データは存在しないので、補助情報の削除をスキップします。")
178
177
  continue
179
178
  input_data_name = input_data["input_data_name"]
180
179
 
@@ -202,9 +201,8 @@ class DeleteSupplementaryDataMain(CommandLineWithConfirm):
202
201
  f"input_data_name='{input_data_name}') "
203
202
  )
204
203
 
205
- except Exception as e: # pylint: disable=broad-except
206
- logger.warning(e)
207
- logger.warning(f"入力データ(input_data_id={input_data_id})配下の補助情報の削除に失敗しました。")
204
+ except Exception: # pylint: disable=broad-except
205
+ logger.warning(f"入力データ(input_data_id='{input_data_id}')配下の補助情報の削除に失敗しました。", exc_info=True)
208
206
 
209
207
  logger.info(f"{len(dict_deleted_count)} / {len(input_data_id_list)} 件の入力データに紐づく補助情報を削除しました。")
210
208
 
@@ -1,8 +1,8 @@
1
1
  import argparse
2
2
  import json
3
3
  import logging
4
+ import re
4
5
  import sys
5
- import uuid
6
6
  from dataclasses import dataclass
7
7
  from functools import partial
8
8
  from multiprocessing import Pool
@@ -32,6 +32,14 @@ from annofabcli.common.utils import get_file_scheme_path
32
32
  logger = logging.getLogger(__name__)
33
33
 
34
34
 
35
+ def convert_supplementary_data_name_to_supplementary_data_id(supplementary_data_name: str) -> str:
36
+ """
37
+ 補助情報データ名から、補助情報データIDを生成します。
38
+ * IDに使えない文字以外は`__`に変換する。
39
+ """
40
+ return re.sub(r"[^a-zA-Z0-9_.-]", "__", supplementary_data_name)
41
+
42
+
35
43
  @dataclass
36
44
  class CsvSupplementaryData(DataClassJsonMixin):
37
45
  """
@@ -71,13 +79,13 @@ class SubPutSupplementaryData:
71
79
  all_yes:
72
80
  """
73
81
 
74
- def __init__(self, service: annofabapi.Resource, facade: AnnofabApiFacade, all_yes: bool = False) -> None: # noqa: FBT001, FBT002
82
+ def __init__(self, service: annofabapi.Resource, facade: AnnofabApiFacade, *, all_yes: bool = False) -> None:
75
83
  self.service = service
76
84
  self.facade = facade
77
85
  self.all_yes = all_yes
78
86
  self.supplementary_data_cache: dict[str, list[SupplementaryData]] = {}
79
87
 
80
- def put_supplementary_data(self, project_id: str, supplementary_data: SupplementaryDataForPut): # noqa: ANN201
88
+ def put_supplementary_data(self, project_id: str, supplementary_data: SupplementaryDataForPut) -> None:
81
89
  file_path = get_file_scheme_path(supplementary_data.supplementary_data_path)
82
90
  if file_path is not None:
83
91
  request_body = {
@@ -89,7 +97,12 @@ class SubPutSupplementaryData:
89
97
  if supplementary_data.supplementary_data_type is not None:
90
98
  request_body.update({"supplementary_data_type": supplementary_data.supplementary_data_type})
91
99
 
92
- logger.debug(f"'{file_path}'を補助情報として登録します。supplementary_data_name='{supplementary_data.supplementary_data_name}'")
100
+ logger.debug(
101
+ f"'{file_path}'を補助情報として登録します。 :: "
102
+ f"input_data_id='{supplementary_data.input_data_id}', "
103
+ f"supplementary_data_id='{supplementary_data.supplementary_data_id}', "
104
+ f"supplementary_data_name='{supplementary_data.supplementary_data_name}'"
105
+ )
93
106
  self.service.wrapper.put_supplementary_data_from_file(
94
107
  project_id,
95
108
  input_data_id=supplementary_data.input_data_id,
@@ -141,10 +154,20 @@ class SubPutSupplementaryData:
141
154
 
142
155
  return yes
143
156
 
144
- def confirm_put_supplementary_data(self, csv_supplementary_data: CsvSupplementaryData, already_exists: bool = False) -> bool: # noqa: FBT001, FBT002
145
- message_for_confirm = f"supplementary_data_name='{csv_supplementary_data.supplementary_data_name}' の補助情報を登録しますか?"
157
+ def confirm_put_supplementary_data(
158
+ self, csv_supplementary_data: CsvSupplementaryData, supplementary_data_id: str, *, already_exists: bool = False
159
+ ) -> bool:
146
160
  if already_exists:
147
- message_for_confirm += f"supplementary_data_id={csv_supplementary_data.supplementary_data_id} を上書きします。"
161
+ message_for_confirm = (
162
+ f"supplementary_data_name='{csv_supplementary_data.supplementary_data_name}', "
163
+ f"supplementary_data_id='{supplementary_data_id}'の補助情報を更新しますか?"
164
+ )
165
+ else:
166
+ message_for_confirm = (
167
+ f"supplementary_data_name='{csv_supplementary_data.supplementary_data_name}', "
168
+ f"supplementary_data_id='{supplementary_data_id}'の補助情報を登録しますか?"
169
+ )
170
+
148
171
  return self.confirm_processing(message_for_confirm)
149
172
 
150
173
  def get_supplementary_data_list_cached(self, project_id: str, input_data_id: str) -> list[SupplementaryData]:
@@ -158,14 +181,14 @@ class SubPutSupplementaryData:
158
181
  cached_list = self.get_supplementary_data_list_cached(project_id, input_data_id)
159
182
  return first_true(cached_list, pred=lambda e: e["supplementary_data_id"] == supplementary_data_id)
160
183
 
161
- def get_supplementary_data_by_number(self, project_id: str, input_data_id: str, supplementary_data_number: int) -> Optional[SupplementaryData]:
162
- cached_list = self.get_supplementary_data_list_cached(project_id, input_data_id)
163
- return first_true(cached_list, pred=lambda e: e["supplementary_data_number"] == supplementary_data_number)
164
-
165
- def put_supplementary_data_main(self, project_id: str, csv_supplementary_data: CsvSupplementaryData, overwrite: bool = False) -> bool: # noqa: FBT001, FBT002
184
+ def put_supplementary_data_main(self, project_id: str, csv_supplementary_data: CsvSupplementaryData, *, overwrite: bool = False) -> bool:
166
185
  last_updated_datetime = None
167
186
  input_data_id = csv_supplementary_data.input_data_id
168
- supplementary_data_id = csv_supplementary_data.supplementary_data_id
187
+ supplementary_data_id = (
188
+ csv_supplementary_data.supplementary_data_id
189
+ if csv_supplementary_data.supplementary_data_id is not None
190
+ else convert_supplementary_data_name_to_supplementary_data_id(csv_supplementary_data.supplementary_data_name)
191
+ )
169
192
  supplementary_data_path = csv_supplementary_data.supplementary_data_path
170
193
 
171
194
  # input_data_idの存在確認
@@ -173,21 +196,20 @@ class SubPutSupplementaryData:
173
196
  logger.warning(f"input_data_id='{input_data_id}'である入力データは存在しないため、補助情報の登録をスキップします。")
174
197
  return False
175
198
 
176
- if supplementary_data_id is not None:
177
- old_supplementary_data_key = f"supplementary_data_id={supplementary_data_id}"
178
- old_supplementary_data = self.get_supplementary_data_by_id(project_id, input_data_id, supplementary_data_id)
179
- else:
180
- supplementary_data_number = csv_supplementary_data.supplementary_data_number
181
- old_supplementary_data_key = f"input_data_id={input_data_id}, supplementary_data_number={supplementary_data_number}"
182
- old_supplementary_data = self.get_supplementary_data_by_number(project_id, input_data_id, supplementary_data_number)
183
- supplementary_data_id = old_supplementary_data["supplementary_data_id"] if old_supplementary_data is not None else str(uuid.uuid4())
199
+ old_supplementary_data = self.get_supplementary_data_by_id(project_id, input_data_id, supplementary_data_id)
184
200
 
185
201
  if old_supplementary_data is not None:
186
202
  if overwrite:
187
- logger.debug(f"'{old_supplementary_data_key}' はすでに存在します。")
203
+ logger.debug(
204
+ f"supplementary_data_id='{supplementary_data_id}'である補助情報がすでに存在します。 :: "
205
+ f"input_data_id='{input_data_id}', supplementary_data_name='{csv_supplementary_data.supplementary_data_name}'"
206
+ )
188
207
  last_updated_datetime = old_supplementary_data["updated_datetime"]
189
208
  else:
190
- logger.debug(f"'{old_supplementary_data_key}' がすでに存在するのでスキップします。")
209
+ logger.debug(
210
+ f"supplementary_data_id='{supplementary_data_id}'である補助情報がすでに存在するので、補助情報の登録をスキップします。 :: "
211
+ f"input_data_id='{input_data_id}', supplementary_data_name='{csv_supplementary_data.supplementary_data_name}'"
212
+ )
191
213
  return False
192
214
 
193
215
  file_path = get_file_scheme_path(supplementary_data_path)
@@ -197,7 +219,7 @@ class SubPutSupplementaryData:
197
219
  logger.warning(f"'{supplementary_data_path}' は存在しません。")
198
220
  return False
199
221
 
200
- if not self.confirm_put_supplementary_data(csv_supplementary_data, already_exists=last_updated_datetime is not None):
222
+ if not self.confirm_put_supplementary_data(csv_supplementary_data, supplementary_data_id, already_exists=last_updated_datetime is not None):
201
223
  return False
202
224
 
203
225
  # 補助情報を登録
@@ -213,8 +235,8 @@ class SubPutSupplementaryData:
213
235
  try:
214
236
  self.put_supplementary_data(project_id, supplementary_data_for_put)
215
237
  logger.debug(
216
- f"補助情報を登録しました。"
217
- f"input_data_id='{supplementary_data_for_put.input_data_id}',"
238
+ f"補助情報を登録しました。 :: "
239
+ f"input_data_id='{supplementary_data_for_put.input_data_id}', "
218
240
  f"supplementary_data_id='{supplementary_data_for_put.supplementary_data_id}', "
219
241
  f"supplementary_data_name='{supplementary_data_for_put.supplementary_data_name}'"
220
242
  )
@@ -222,7 +244,7 @@ class SubPutSupplementaryData:
222
244
 
223
245
  except requests.exceptions.HTTPError:
224
246
  logger.warning(
225
- f"補助情報の登録に失敗しました。"
247
+ f"補助情報の登録に失敗しました。 ::"
226
248
  f"input_data_id='{supplementary_data_for_put.input_data_id}',"
227
249
  f"supplementary_data_id='{supplementary_data_for_put.supplementary_data_id}', "
228
250
  f"supplementary_data_name='{supplementary_data_for_put.supplementary_data_name}'",
@@ -240,7 +262,8 @@ class PutSupplementaryData(CommandLine):
240
262
  self,
241
263
  project_id: str,
242
264
  supplementary_data_list: list[CsvSupplementaryData],
243
- overwrite: bool = False, # noqa: FBT001, FBT002
265
+ *,
266
+ overwrite: bool = False,
244
267
  parallelism: Optional[int] = None,
245
268
  ) -> None:
246
269
  """
@@ -280,7 +303,7 @@ class PutSupplementaryData(CommandLine):
280
303
 
281
304
  @staticmethod
282
305
  def get_supplementary_data_list_from_csv(csv_path: Path) -> list[CsvSupplementaryData]:
283
- def create_supplementary_data(e: Any): # noqa: ANN202, ANN401
306
+ def create_supplementary_data(e: Any) -> CsvSupplementaryData: # noqa: ANN401
284
307
  supplementary_data_id = e.supplementary_data_id if not pandas.isna(e.supplementary_data_id) else None
285
308
  supplementary_data_type = e.supplementary_data_type if not pandas.isna(e.supplementary_data_type) else None
286
309
  return CsvSupplementaryData(
@@ -309,9 +309,10 @@ class ListTasksAddedTaskHistoryMain:
309
309
  task_list = list_task_obj.get_task_list(self.project_id, task_id_list=task_id_list, task_query=task_query)
310
310
 
311
311
  obj = AddingAdditionalInfoToTask(self.service, project_id=self.project_id)
312
+ logger.info(f"{len(task_list)} 件のタスクの履歴情報を取得します。")
312
313
 
313
314
  for index, task in enumerate(task_list):
314
- if (index + 1) % 1000 == 0:
315
+ if (index + 1) % 100 == 0:
315
316
  logger.debug(f"{index + 1} 件目のタスク履歴情報を取得します。")
316
317
 
317
318
  obj.add_additional_info_to_task(task)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: annofabcli
3
- Version: 1.95.0
3
+ Version: 1.96.1
4
4
  Summary: Utility Command Line Interface for AnnoFab
5
5
  Home-page: https://github.com/kurusugawa-computer/annofab-cli
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  annofabcli/__init__.py,sha256=NMA7kFxmLlCiILQPHJa9mEuqXxtLALw_dwyXYsvz4VM,71
2
2
  annofabcli/__main__.py,sha256=JzfycqVG9ENhWOCxTouZwpHwWTSrI-grLsaMudxjyBM,5283
3
- annofabcli/__version__.py,sha256=qh48jc0AerpxMn6bU6jgOxQqy6_nPvZpVTJ5jCGYCDI,132
3
+ annofabcli/__version__.py,sha256=fRMeiNR_DG_Pyx4KmlvY_dwyMYU17Xi-HyO9fioFt3g,132
4
4
  annofabcli/annotation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  annofabcli/annotation/annotation_query.py,sha256=ke3W3RT1-WfFzwt-TXcQwGmghG34vcKJkM_jxgbNKjU,15922
6
6
  annofabcli/annotation/change_annotation_attributes.py,sha256=zHXyENZfbMGL_15xiK7Cy4cQ2sV0GjSVmKuPm3sOX7Y,17173
@@ -15,7 +15,7 @@ annofabcli/annotation/list_annotation_count.py,sha256=T9fbaoxWeDJIVgW_YgHRldbwrV
15
15
  annofabcli/annotation/restore_annotation.py,sha256=naUEbt48ION9JSijCBR2aQdaoCrRu005tYq0vgUtyp0,14683
16
16
  annofabcli/annotation/subcommand_annotation.py,sha256=Xu4SOHLpo_kPZbvE8hQHr9NRduPTESqu7pYC4LGDlXg,1872
17
17
  annofabcli/annotation_specs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
- annofabcli/annotation_specs/export_annotation_specs.py,sha256=CGmcyP8DbfFUcWnJgJSgxYS9FkV3mW15zZPDab7LNBE,4685
18
+ annofabcli/annotation_specs/export_annotation_specs.py,sha256=eZF5fj2P5U22_5UKgbpsUhZEUvVPMPerMpN4miIcnSA,4821
19
19
  annofabcli/annotation_specs/get_annotation_specs_with_attribute_id_replaced.py,sha256=L0H0M31Amnm3ZEg9JdQe2A1vprsCLPTpj0LUFnP6GDY,8330
20
20
  annofabcli/annotation_specs/get_annotation_specs_with_choice_id_replaced.py,sha256=mISJAgRTBMvISPk5__0w0MmhTkyQCcGPDin8K97x5Eg,6943
21
21
  annofabcli/annotation_specs/get_annotation_specs_with_label_id_replaced.py,sha256=5tlvSdUXAUn8uQ1BXNWxkR2MIKnV6AJoxveiwD21IAo,7114
@@ -75,7 +75,7 @@ annofabcli/input_data/download_input_data_json.py,sha256=vxGoeM3ZEggQbWiWsrDK0_G
75
75
  annofabcli/input_data/list_all_input_data.py,sha256=Kq261WCkma5UNKfMDT7O6Z-dzzqp-KP1wL0DvHe5fH8,9879
76
76
  annofabcli/input_data/list_all_input_data_merged_task.py,sha256=UaBJW-nMHytmQ4okg69Ew1jhC2vMNnRMgl2lEBabtUI,12850
77
77
  annofabcli/input_data/list_input_data.py,sha256=RBxsHyKg1bVIEQUFDkfrq-nJmEdEYNoCjJ2L2GgSfeU,11519
78
- annofabcli/input_data/put_input_data.py,sha256=bEbo6rmLkmYTUp-ynoGUXpZoYLdBDLaZYcrxqEm74tw,17951
78
+ annofabcli/input_data/put_input_data.py,sha256=x54C-rLJVzr1YF2GlMR0w0HJReOE3E7YKiBeuh0RsTI,17934
79
79
  annofabcli/input_data/put_input_data_with_zip.py,sha256=SA4aMAwMBFgc9Lh0zmRCbmkXG4AMrcBqd5zeTSdr8lc,5566
80
80
  annofabcli/input_data/subcommand_input_data.py,sha256=X8EoxsF6PMiKrvk_r7PIe2D0WZuaPlgLJRuTiljPIdM,2048
81
81
  annofabcli/input_data/update_metadata_of_input_data.py,sha256=txdliirBrtoTabGEyWYLPclZC_DarEpRAY3MTsbViPA,11556
@@ -130,12 +130,13 @@ annofabcli/stat_visualization/write_performance_rating_csv.py,sha256=TDn7-poyFt2
130
130
  annofabcli/statistics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
131
131
  annofabcli/statistics/histogram.py,sha256=CvzDxT2cKLSnBGSqkZE6p92PayGxYYja1YyB24M4ALU,3245
132
132
  annofabcli/statistics/linegraph.py,sha256=0kr7jVBNMiM2ECYhv3Ry5RitElKerSl9ZKxbKzfiplI,12494
133
+ annofabcli/statistics/list_annotation_attribute.py,sha256=87jjNCOXJUbWnmswMCLN7GTjGsBfqpFJ6hViWmnj8Y4,12557
133
134
  annofabcli/statistics/list_annotation_count.py,sha256=xsqXxRO21MW7Wbs96PEa8gHb3i7bxjQvoMHLB2eT-0g,50360
134
135
  annofabcli/statistics/list_annotation_duration.py,sha256=9PCg1IA_g4HoHHSfsMVb2zBMLfLJ3u9Id8Qa9L8Qhko,31923
135
136
  annofabcli/statistics/list_video_duration.py,sha256=uNeMteRBX2JG_AWmcgMJj0Jzbq_qF7bvAwr25GmeIiw,9124
136
137
  annofabcli/statistics/list_worktime.py,sha256=C7Yu3IOW2EvhkJJv6gY3hNdS9_TOLmT_9LZsB7vLJ1o,6493
137
138
  annofabcli/statistics/scatter.py,sha256=IUCwXix9GbZb6V82wjjb5q2eamrT5HQsU_bzDTjAFnM,11011
138
- annofabcli/statistics/subcommand_statistics.py,sha256=Exf7fpOnz8jezBEDKdTwVN7_upN0jzwDt8g0kS9H0Hw,2129
139
+ annofabcli/statistics/subcommand_statistics.py,sha256=JPixzhJGClQYVH6Tgby3KTD85df-aEJKvLAjfJCv-1E,2259
139
140
  annofabcli/statistics/summarize_task_count.py,sha256=8OH6BBRYRjHJkWRTjU0A0OfXa7f3NIRHrxPNFlRt_hM,9707
140
141
  annofabcli/statistics/summarize_task_count_by_task_id_group.py,sha256=TSSmcFv615NLcq6uqXmg3ilYqSHl3A5qp90msVQM1gE,8646
141
142
  annofabcli/statistics/summarize_task_count_by_user.py,sha256=TRoJXpt2HOVb8QO2YtRejkOAxyK80_NsPt3Vk9es9C8,6948
@@ -166,9 +167,9 @@ annofabcli/statistics/visualize_annotation_duration.py,sha256=J4Z3Ql1sdiTraMnRaR
166
167
  annofabcli/statistics/visualize_statistics.py,sha256=3bP9WRALADkAxPePa1Ix49apahi1V_qjAL6Dbrwtxd0,39550
167
168
  annofabcli/statistics/visualize_video_duration.py,sha256=A6Q91aWx7GO4F0TGxbiQI0za8BFr7IqiUaYyw3__BgY,16842
168
169
  annofabcli/supplementary/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
169
- annofabcli/supplementary/delete_supplementary_data.py,sha256=StHGH7qB6mEtuD75FKRQRFdTY2Lfhnl6s4u1Jq8b-MA,13756
170
+ annofabcli/supplementary/delete_supplementary_data.py,sha256=QeOohZ6RnsxC9rKSPi0JJqJXmj99ZL17psulTqlEQFU,13738
170
171
  annofabcli/supplementary/list_supplementary_data.py,sha256=F4iJnQi_4W7S_d7ahqxWFSFcnfiKYhNuysC28v0QUWA,7649
171
- annofabcli/supplementary/put_supplementary_data.py,sha256=KLGFUIYfHmB39U7qhj6y6exU9D6XMDmYpYBKzkIxqiU,19130
172
+ annofabcli/supplementary/put_supplementary_data.py,sha256=FwFRszjcsJt-UrT2RCRAs4zzYea03CbDoiKBUZQW0zc,19581
172
173
  annofabcli/supplementary/subcommand_supplementary.py,sha256=F8qfuNQzgW5HV1QKB4h0DWN7-kPVQcoFQwPfW_vjZVk,1079
173
174
  annofabcli/task/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
174
175
  annofabcli/task/cancel_acceptance.py,sha256=rHKmAzQE1a25szVWb7dexokeW32V12bkyDIYM_oIl5U,13899
@@ -183,7 +184,7 @@ annofabcli/task/download_task_json.py,sha256=Ocjecmdf2WV_Sq3u1InfMLIsT3XSw0ojyJm
183
184
  annofabcli/task/list_all_tasks.py,sha256=F9GpzzgWffF3lUeGrFIvjweq-iEwJ1c-g8usskO_2dE,6506
184
185
  annofabcli/task/list_all_tasks_added_task_history.py,sha256=fkdiuo64iS7xxvIfGKzSiUPPEMiCVnJjjcAtMxe2Ngs,9551
185
186
  annofabcli/task/list_tasks.py,sha256=O4jjp_zdmurcGNWXFp9JXHJsH4nhlR5e3ok96YnD1SI,10237
186
- annofabcli/task/list_tasks_added_task_history.py,sha256=TVyHODV5lQuB6ALs8Xd4UUm4FYf0F6kycv7ajbm0MkQ,19583
187
+ annofabcli/task/list_tasks_added_task_history.py,sha256=7avkLYFErcdSNxGc8CQXr4FFIF1z9FtQJBSloD-tzBI,19675
187
188
  annofabcli/task/put_tasks.py,sha256=hT2xPowJmcNJhjxoAm-MFiKTw_RFcJUYlpeanegVrAU,13400
188
189
  annofabcli/task/put_tasks_by_count.py,sha256=MUHfWhqtSAXnB3O36p3bMSSgQ_3Zek9GT5qRvHGx8Lo,6041
189
190
  annofabcli/task/reject_tasks.py,sha256=5ByAN6VnKwvU5BT_cfsHwA1jLDl74bonqk3bwtnrkPU,23139
@@ -199,8 +200,8 @@ annofabcli/task_history_event/download_task_history_event_json.py,sha256=hQLVbQ0
199
200
  annofabcli/task_history_event/list_all_task_history_event.py,sha256=JQEgwOIXbbTIfeX23AVaoySHViOR9UGm9uoXuhVEBqo,6446
200
201
  annofabcli/task_history_event/list_worktime.py,sha256=9jsRYa2C9bva8E1Aqxv9CCKDuCP0MvbiaIyQFTDpjqY,13150
201
202
  annofabcli/task_history_event/subcommand_task_history_event.py,sha256=mJVJoT4RXk4HWnY7-Nrsl4If-gtaIIEXd2z7eFZwM2I,1260
202
- annofabcli-1.95.0.dist-info/LICENSE,sha256=pcqWYfxFtxBzhvKp3x9MXNM4xciGb2eFewaRhXUNHlo,1081
203
- annofabcli-1.95.0.dist-info/METADATA,sha256=iAatfNNAeWskgaipQQ13ZHVSsEVHvmDTes41DtynbCQ,5626
204
- annofabcli-1.95.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
205
- annofabcli-1.95.0.dist-info/entry_points.txt,sha256=A8vlN9fiMhbYRcdBfSpl7piYzAwvkMhRXIPQUAvQFUo,55
206
- annofabcli-1.95.0.dist-info/RECORD,,
203
+ annofabcli-1.96.1.dist-info/LICENSE,sha256=pcqWYfxFtxBzhvKp3x9MXNM4xciGb2eFewaRhXUNHlo,1081
204
+ annofabcli-1.96.1.dist-info/METADATA,sha256=u94b-Jzx5b2TOxFlJeBqtJbcbkkBxEgCtLomabfLhP4,5626
205
+ annofabcli-1.96.1.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
206
+ annofabcli-1.96.1.dist-info/entry_points.txt,sha256=A8vlN9fiMhbYRcdBfSpl7piYzAwvkMhRXIPQUAvQFUo,55
207
+ annofabcli-1.96.1.dist-info/RECORD,,