annofabcli 1.104.1__py3-none-any.whl → 1.105.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/annotation_query.py +5 -0
- annofabcli/annotation/change_annotation_attributes_per_annotation.py +296 -0
- annofabcli/annotation/delete_annotation.py +1 -2
- annofabcli/annotation/dump_annotation.py +31 -13
- annofabcli/annotation/restore_annotation.py +45 -5
- annofabcli/annotation/subcommand_annotation.py +2 -0
- annofabcli/comment/put_comment.py +1 -1
- annofabcli/common/visualize.py +3 -6
- annofabcli/task_history_event/list_all_task_history_event.py +25 -7
- annofabcli/task_history_event/list_worktime.py +71 -12
- {annofabcli-1.104.1.dist-info → annofabcli-1.105.1.dist-info}/METADATA +1 -1
- {annofabcli-1.104.1.dist-info → annofabcli-1.105.1.dist-info}/RECORD +15 -14
- {annofabcli-1.104.1.dist-info → annofabcli-1.105.1.dist-info}/WHEEL +0 -0
- {annofabcli-1.104.1.dist-info → annofabcli-1.105.1.dist-info}/entry_points.txt +0 -0
- {annofabcli-1.104.1.dist-info → annofabcli-1.105.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -105,6 +105,11 @@ def _get_additional_data_v2(additional_data: dict[str, Any], attribute_value: At
|
|
|
105
105
|
return get_english_message(additional_data["name"])
|
|
106
106
|
|
|
107
107
|
additional_data_definition_id = additional_data["additional_data_definition_id"]
|
|
108
|
+
if attribute_value is None:
|
|
109
|
+
return {
|
|
110
|
+
"definition_id": additional_data_definition_id,
|
|
111
|
+
"value": None,
|
|
112
|
+
}
|
|
108
113
|
|
|
109
114
|
additional_data_type: str = additional_data["type"]
|
|
110
115
|
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Optional, Union
|
|
10
|
+
|
|
11
|
+
import annofabapi
|
|
12
|
+
import pandas
|
|
13
|
+
from annofabapi.models import ProjectMemberRole
|
|
14
|
+
from annofabapi.pydantic_models.task_status import TaskStatus
|
|
15
|
+
from pydantic import BaseModel
|
|
16
|
+
|
|
17
|
+
import annofabcli
|
|
18
|
+
from annofabcli.annotation.annotation_query import convert_attributes_from_cli_to_additional_data_list_v2
|
|
19
|
+
from annofabcli.annotation.dump_annotation import DumpAnnotationMain
|
|
20
|
+
from annofabcli.common.cli import (
|
|
21
|
+
COMMAND_LINE_ERROR_STATUS_CODE,
|
|
22
|
+
ArgumentParser,
|
|
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
|
+
Attributes = dict[str, Optional[Union[str, int, bool]]]
|
|
34
|
+
"""属性情報"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TargetAnnotation(BaseModel):
|
|
38
|
+
task_id: str
|
|
39
|
+
input_data_id: str
|
|
40
|
+
annotation_id: str
|
|
41
|
+
attributes: Attributes
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_annotation_list_per_task_id_input_data_id(anno_list: list[TargetAnnotation]) -> dict[str, dict[str, list[TargetAnnotation]]]:
|
|
45
|
+
"""
|
|
46
|
+
タスクIDと入力データIDごとにアノテーションをグループ化する。
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
anno_list: アノテーションのリスト
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
タスクIDと入力データIDをキーとしたアノテーションの辞書
|
|
53
|
+
"""
|
|
54
|
+
grouped: dict[str, dict[str, list[TargetAnnotation]]] = defaultdict(lambda: defaultdict(list))
|
|
55
|
+
|
|
56
|
+
for annotation in anno_list:
|
|
57
|
+
grouped[annotation.task_id][annotation.input_data_id].append(annotation)
|
|
58
|
+
return grouped
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ChangeAnnotationAttributesPerAnnotationMain(CommandLineWithConfirm):
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
service: annofabapi.Resource,
|
|
65
|
+
*,
|
|
66
|
+
project_id: str,
|
|
67
|
+
is_force: bool,
|
|
68
|
+
all_yes: bool,
|
|
69
|
+
backup_dir: Optional[Path] = None,
|
|
70
|
+
) -> None:
|
|
71
|
+
self.service = service
|
|
72
|
+
self.project_id = project_id
|
|
73
|
+
self.is_force = is_force
|
|
74
|
+
self.backup_dir = backup_dir
|
|
75
|
+
self.annotation_specs, _ = self.service.api.get_annotation_specs(project_id, query_params={"v": "3"})
|
|
76
|
+
self.dump_annotation_obj = DumpAnnotationMain(service, project_id)
|
|
77
|
+
super().__init__(all_yes)
|
|
78
|
+
|
|
79
|
+
def change_annotation_attributes_by_frame(self, task_id: str, input_data_id: str, anno_list: list[TargetAnnotation]) -> bool:
|
|
80
|
+
"""
|
|
81
|
+
フレームごとにアノテーション属性値を変更する。
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
task_id: タスクID
|
|
85
|
+
input_data_id: 入力データID
|
|
86
|
+
additional_data_list: 変更後の属性値(`AdditionalDataListV2`スキーマ)
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
editor_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id=task_id, input_data_id=input_data_id, query_params={"v": "2"})
|
|
90
|
+
|
|
91
|
+
if self.backup_dir is not None:
|
|
92
|
+
(self.backup_dir / task_id).mkdir(exist_ok=True, parents=True)
|
|
93
|
+
self.dump_annotation_obj.dump_editor_annotation(editor_annotation, json_path=self.backup_dir / task_id / f"{input_data_id}.json")
|
|
94
|
+
|
|
95
|
+
details_map = {detail["annotation_id"]: detail for detail in editor_annotation["details"]}
|
|
96
|
+
|
|
97
|
+
def _to_request_body_elm(anno: TargetAnnotation) -> dict[str, Any]:
|
|
98
|
+
additional_data_list = convert_attributes_from_cli_to_additional_data_list_v2(anno.attributes, annotation_specs=self.annotation_specs)
|
|
99
|
+
return {
|
|
100
|
+
"data": {
|
|
101
|
+
"project_id": editor_annotation["project_id"],
|
|
102
|
+
"task_id": editor_annotation["task_id"],
|
|
103
|
+
"input_data_id": editor_annotation["input_data_id"],
|
|
104
|
+
"updated_datetime": editor_annotation["updated_datetime"],
|
|
105
|
+
"annotation_id": anno.annotation_id,
|
|
106
|
+
"label_id": details_map[anno.annotation_id]["label_id"],
|
|
107
|
+
"additional_data_list": additional_data_list,
|
|
108
|
+
},
|
|
109
|
+
"_type": "PutV2",
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
request_body = [_to_request_body_elm(annotation) for annotation in anno_list]
|
|
113
|
+
|
|
114
|
+
self.service.api.batch_update_annotations(self.project_id, request_body=request_body)
|
|
115
|
+
logger.debug(f"task_id='{task_id}', input_data_id='{input_data_id}' :: {len(request_body)}件の属性値を変更しました。")
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
def change_annotation_attributes_for_task(self, task_id: str, annotation_list_per_input_data_id: dict[str, list[TargetAnnotation]]) -> tuple[bool, int, int]:
|
|
119
|
+
"""
|
|
120
|
+
1個のタスクに含まれるアノテーションの属性値を変更する。
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
task_id: タスクID
|
|
124
|
+
annotation_list_per_input_data_id: 入力データIDごとのアノテーションリスト
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
tuple:
|
|
128
|
+
[0]: 属性値の変更可能なタスクかどうか
|
|
129
|
+
[1]: 属性値の変更に成功したアノテーション数
|
|
130
|
+
[2]: 属性値を変更できなかったアノテーション数
|
|
131
|
+
|
|
132
|
+
"""
|
|
133
|
+
annotation_count = sum(len(v) for v in annotation_list_per_input_data_id.values())
|
|
134
|
+
task = self.service.wrapper.get_task_or_none(self.project_id, task_id)
|
|
135
|
+
|
|
136
|
+
succeed_to_change_annotation_count = 0
|
|
137
|
+
failed_to_change_annotation_count = 0
|
|
138
|
+
|
|
139
|
+
if task is None:
|
|
140
|
+
logger.warning(f"task_id='{task_id}' :: タスクが存在しないため、{annotation_count} 件のアノテーションの属性値の変更をスキップします。")
|
|
141
|
+
return False, 0, annotation_count
|
|
142
|
+
|
|
143
|
+
if task["status"] == TaskStatus.WORKING.value:
|
|
144
|
+
logger.info(f"task_id='{task_id}' :: タスクが作業中状態のため、{annotation_count} 件のアノテーションの属性値の変更をスキップします。")
|
|
145
|
+
failed_to_change_annotation_count += annotation_count
|
|
146
|
+
return False, 0, annotation_count
|
|
147
|
+
|
|
148
|
+
if not self.is_force: # noqa: SIM102
|
|
149
|
+
if task["status"] == TaskStatus.COMPLETE.value:
|
|
150
|
+
logger.info(
|
|
151
|
+
f"task_id='{task_id}' :: タスクが完了状態のため、アノテーション {annotation_count} 件のアノテーションの属性値の変更をスキップします。"
|
|
152
|
+
f"完了状態のタスクのアノテーションを削除するには、`--force`オプションを指定してください。"
|
|
153
|
+
)
|
|
154
|
+
failed_to_change_annotation_count += annotation_count
|
|
155
|
+
return False, 0, annotation_count
|
|
156
|
+
|
|
157
|
+
if not self.confirm_processing(f"task_id='{task_id}'に含まれるアノテーション{annotation_count}件の属性値を変更しますか?"):
|
|
158
|
+
return False, 0, annotation_count
|
|
159
|
+
|
|
160
|
+
for input_data_id, sub_anno_list in annotation_list_per_input_data_id.items():
|
|
161
|
+
try:
|
|
162
|
+
if self.change_annotation_attributes_by_frame(task_id, input_data_id, sub_anno_list):
|
|
163
|
+
succeed_to_change_annotation_count += len(sub_anno_list)
|
|
164
|
+
else:
|
|
165
|
+
failed_to_change_annotation_count += len(sub_anno_list)
|
|
166
|
+
except Exception:
|
|
167
|
+
logger.warning(f"task_id='{task_id}', input_data_id='{input_data_id}' :: アノテーションの属性値変更に失敗しました。", exc_info=True)
|
|
168
|
+
failed_to_change_annotation_count += len(sub_anno_list)
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
return True, succeed_to_change_annotation_count, failed_to_change_annotation_count
|
|
172
|
+
|
|
173
|
+
def change_annotation_attributes(self, anno_list: list[TargetAnnotation]) -> None:
|
|
174
|
+
"""
|
|
175
|
+
アノテーションごとに属性値を変更する。
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
anno_list: 各アノテーションの変更内容リスト
|
|
179
|
+
"""
|
|
180
|
+
changed_task_count = 0
|
|
181
|
+
failed_to_change_annotation_count = 0
|
|
182
|
+
annotation_list_per_task_id_input_data_id = get_annotation_list_per_task_id_input_data_id(anno_list)
|
|
183
|
+
|
|
184
|
+
total_task_count = len(annotation_list_per_task_id_input_data_id)
|
|
185
|
+
total_annotation_count = len(anno_list)
|
|
186
|
+
|
|
187
|
+
for task_id, input_data_dict in annotation_list_per_task_id_input_data_id.items():
|
|
188
|
+
is_changeable_task, succeed_to_change_annotation_count, failed_to_change_annotation_count = self.change_annotation_attributes_for_task(task_id, input_data_dict)
|
|
189
|
+
if is_changeable_task:
|
|
190
|
+
changed_task_count += 1
|
|
191
|
+
|
|
192
|
+
logger.info(
|
|
193
|
+
f"{succeed_to_change_annotation_count}/{total_annotation_count} 件のアノテーションの属性値を変更しました。 :: "
|
|
194
|
+
f"アノテーションが変更されたタスク数は {changed_task_count}/{total_task_count} 件です。"
|
|
195
|
+
f"{failed_to_change_annotation_count} 件のアノテーションは変更できませんでした。"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class ChangeAttributesPerAnnotation(CommandLine):
|
|
200
|
+
"""
|
|
201
|
+
アノテーションごとに属性値を個別変更
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
COMMON_MESSAGE = "annofabcli annotation change_attributes_per_annotation: error:"
|
|
205
|
+
|
|
206
|
+
def main(self) -> None:
|
|
207
|
+
args = self.args
|
|
208
|
+
|
|
209
|
+
if args.json is not None:
|
|
210
|
+
annotation_items = get_json_from_args(args.json)
|
|
211
|
+
if not isinstance(annotation_items, list):
|
|
212
|
+
print(f"{self.COMMON_MESSAGE} argument --json: JSON形式が不正です。オブジェクトの配列を指定してください。", file=sys.stderr) # noqa: T201
|
|
213
|
+
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
214
|
+
|
|
215
|
+
target_annotation_list = [TargetAnnotation.model_validate(anno) for anno in annotation_items]
|
|
216
|
+
|
|
217
|
+
elif args.csv is not None:
|
|
218
|
+
df_input = pandas.read_csv(args.csv)
|
|
219
|
+
target_annotation_list = [
|
|
220
|
+
TargetAnnotation(task_id=e["task_id"], input_data_id=e["input_data_id"], annotation_id=e["annotation_id"], attributes=json.loads(e["attributes"]))
|
|
221
|
+
for e in df_input.to_dict(orient="records")
|
|
222
|
+
]
|
|
223
|
+
else:
|
|
224
|
+
print(f"{self.COMMON_MESSAGE} argument '--json' または '--csv' のいずれかを指定してください。", file=sys.stderr) # noqa: T201
|
|
225
|
+
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
226
|
+
|
|
227
|
+
project_id = args.project_id
|
|
228
|
+
|
|
229
|
+
if args.backup is None:
|
|
230
|
+
print( # noqa: T201
|
|
231
|
+
"間違えてアノテーションを変更してしまっときに復元できるようにするため、'--backup'でバックアップ用のディレクトリを指定することを推奨します。",
|
|
232
|
+
file=sys.stderr,
|
|
233
|
+
)
|
|
234
|
+
if not self.confirm_processing("復元用のバックアップディレクトリが指定されていません。処理を続行しますか?"):
|
|
235
|
+
sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
|
|
236
|
+
backup_dir = None
|
|
237
|
+
else:
|
|
238
|
+
backup_dir = Path(args.backup)
|
|
239
|
+
|
|
240
|
+
# プロジェクト権限チェック
|
|
241
|
+
super().validate_project(project_id, [ProjectMemberRole.OWNER, ProjectMemberRole.ACCEPTER])
|
|
242
|
+
|
|
243
|
+
main_obj = ChangeAnnotationAttributesPerAnnotationMain(self.service, project_id=project_id, all_yes=args.yes, is_force=args.force, backup_dir=backup_dir)
|
|
244
|
+
main_obj.change_annotation_attributes(target_annotation_list)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def main(args: argparse.Namespace) -> None:
|
|
248
|
+
service = build_annofabapi_resource_and_login(args)
|
|
249
|
+
facade = AnnofabApiFacade(service)
|
|
250
|
+
ChangeAttributesPerAnnotation(service, facade, args).main()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
254
|
+
argument_parser = ArgumentParser(parser)
|
|
255
|
+
argument_parser.add_project_id()
|
|
256
|
+
|
|
257
|
+
sample_json_obj = [{"task_id": "t1", "input_data_id": "i1", "annotation_id": "a1", "attributes": {"occluded": True}}]
|
|
258
|
+
input_group = parser.add_mutually_exclusive_group(required=True)
|
|
259
|
+
input_group.add_argument(
|
|
260
|
+
"--json",
|
|
261
|
+
type=str,
|
|
262
|
+
help="各アノテーションごとに変更内容を記載したJSONリストを指定します。 ``file://`` を先頭に付けるとJSON形式のファイルを指定できます。\n"
|
|
263
|
+
f"(例) '{json.dumps(sample_json_obj, ensure_ascii=False)}'",
|
|
264
|
+
)
|
|
265
|
+
input_group.add_argument(
|
|
266
|
+
"--csv",
|
|
267
|
+
type=str,
|
|
268
|
+
help="各アノテーションごとに変更内容を記載したCSVファイルを指定します。 \n"
|
|
269
|
+
"* `task_id`, `input_data_id`, `annotation_id`, `attributes` の4つのカラムが必要です。\n"
|
|
270
|
+
f"`attributes` カラムには、属性名と値を '{json.dumps({'occluded': True})}' のようにJSON形式で指定します。\n",
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
parser.add_argument(
|
|
274
|
+
"--force",
|
|
275
|
+
action="store_true",
|
|
276
|
+
help="指定した場合は、完了状態のタスクのアノテーションも属性値を変更します。ただし、完了状態のタスクのアノテーションを変更するには、オーナーロールを持つユーザーが実行する必要があります。",
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
parser.add_argument(
|
|
280
|
+
"--backup",
|
|
281
|
+
type=Path,
|
|
282
|
+
required=False,
|
|
283
|
+
help="アノテーションのバックアップを保存するディレクトリのパス。アノテーションの復元は ``annotation restore`` コマンドで実現できます。",
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
parser.set_defaults(subcommand_func=main)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
|
|
290
|
+
subcommand_name = "change_attributes_per_annotation"
|
|
291
|
+
subcommand_help = "各アノテーションの属性値を変更します。"
|
|
292
|
+
epilog = "オーナロールまたはチェッカーロールを持つユーザで実行してください。"
|
|
293
|
+
|
|
294
|
+
parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, epilog=epilog)
|
|
295
|
+
parse_args(parser)
|
|
296
|
+
return parser
|
|
@@ -296,8 +296,7 @@ class DeleteAnnotationMain(CommandLineWithConfirm):
|
|
|
296
296
|
deleted_task_count += 1
|
|
297
297
|
for input_data_id, annotation_ids in sub_grouped.items():
|
|
298
298
|
# 指定input_data_idの全annotationを取得
|
|
299
|
-
|
|
300
|
-
editor_annotation = self.service.wrapper.get_editor_annotation_or_none(self.project_id, task_id=task_id, input_data_id=input_data_id, query_params={"v": "1"})
|
|
299
|
+
editor_annotation = self.service.wrapper.get_editor_annotation_or_none(self.project_id, task_id=task_id, input_data_id=input_data_id, query_params={"v": "2"})
|
|
301
300
|
if editor_annotation is None:
|
|
302
301
|
logger.warning(
|
|
303
302
|
f"task_id='{task_id}'のタスクに、input_data_id='{input_data_id}'の入力データが含まれていません。 アノテーションの削除をスキップします。 :: annotation_ids={annotation_ids}"
|
|
@@ -27,26 +27,44 @@ class DumpAnnotationMain:
|
|
|
27
27
|
def dump_editor_annotation(self, editor_annotation: dict[str, Any], json_path: Path) -> None:
|
|
28
28
|
"""
|
|
29
29
|
`getEditorAnnotation` APIのレスポンスをファイルに保存する。
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
editor_annotation: v1, v2 のどちらの形式でも対応。
|
|
30
33
|
"""
|
|
31
34
|
json_path.write_text(json.dumps(editor_annotation, ensure_ascii=False), encoding="utf-8")
|
|
32
|
-
|
|
33
35
|
details = editor_annotation["details"]
|
|
34
|
-
outer_details = [e for e in details if e["data_holding_type"] == AnnotationDataHoldingType.OUTER.value]
|
|
35
|
-
if len(outer_details) == 0:
|
|
36
|
-
return
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
if editor_annotation.get("format_version") == "2.0.0":
|
|
38
|
+
outer_details = [e for e in details if e["body"]["_type"] == "Outer"]
|
|
39
|
+
if len(outer_details) == 0:
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
input_data_id = editor_annotation["input_data_id"]
|
|
43
|
+
outer_dir = json_path.parent / input_data_id
|
|
44
|
+
outer_dir.mkdir(exist_ok=True, parents=True)
|
|
45
|
+
# 塗りつぶし画像など外部リソースに保存されているファイルをダウンロードする
|
|
46
|
+
for detail in outer_details:
|
|
47
|
+
annotation_id = detail["annotation_id"]
|
|
48
|
+
outer_file_path = outer_dir / f"{annotation_id}"
|
|
49
|
+
self.service.wrapper.download(detail["body"]["url"], outer_file_path)
|
|
50
|
+
|
|
51
|
+
else:
|
|
52
|
+
outer_details = [e for e in details if e["data_holding_type"] == AnnotationDataHoldingType.OUTER.value]
|
|
53
|
+
if len(outer_details) == 0:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
input_data_id = editor_annotation["input_data_id"]
|
|
57
|
+
outer_dir = json_path.parent / input_data_id
|
|
58
|
+
outer_dir.mkdir(exist_ok=True, parents=True)
|
|
41
59
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
60
|
+
# 塗りつぶし画像など外部リソースに保存されているファイルをダウンロードする
|
|
61
|
+
for detail in outer_details:
|
|
62
|
+
annotation_id = detail["annotation_id"]
|
|
63
|
+
outer_file_path = outer_dir / f"{annotation_id}"
|
|
64
|
+
self.service.wrapper.download(detail["url"], outer_file_path)
|
|
47
65
|
|
|
48
66
|
def dump_annotation_for_input_data(self, task_id: str, input_data_id: str, task_dir: Path) -> None:
|
|
49
|
-
editor_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id)
|
|
67
|
+
editor_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id, query_params={"v": "2"})
|
|
50
68
|
json_path = task_dir / f"{input_data_id}.json"
|
|
51
69
|
self.dump_editor_annotation(editor_annotation=editor_annotation, json_path=json_path)
|
|
52
70
|
|
|
@@ -74,10 +74,17 @@ class RestoreAnnotationMain(CommandLineWithConfirm):
|
|
|
74
74
|
|
|
75
75
|
return detail
|
|
76
76
|
|
|
77
|
-
def
|
|
77
|
+
def editor_annotation_to_request_body_v1(self, editor_annotation: dict[str, Any], parser: SimpleAnnotationParser) -> dict[str, Any]:
|
|
78
|
+
"""
|
|
79
|
+
`get_editor_annotation`で取得したアノテーション(v1)を、`put_annotation` APIに渡すリクエストボディ(v1)に変換する。
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
editor_annotation: `get_editor_annotation`で取得したアノテーション(v1)
|
|
83
|
+
parser: SimpleAnnotationParserインスタンス。アノテーションのファイルを開くために利用する。
|
|
84
|
+
"""
|
|
78
85
|
# infer_missing=Trueを指定する理由:Optional型のキーが存在しない場合でも、AnnotationV1データクラスのインスタンスを生成できるようにするため
|
|
79
86
|
# https://qiita.com/yuji38kwmt/items/c5b56f70da3b8a70ba31
|
|
80
|
-
annotation: AnnotationV1 = AnnotationV1.from_dict(
|
|
87
|
+
annotation: AnnotationV1 = AnnotationV1.from_dict(editor_annotation, infer_missing=True)
|
|
81
88
|
request_details: list[dict[str, Any]] = []
|
|
82
89
|
for detail in annotation.details:
|
|
83
90
|
request_detail = self._to_annotation_detail_for_request(parser, detail)
|
|
@@ -94,15 +101,48 @@ class RestoreAnnotationMain(CommandLineWithConfirm):
|
|
|
94
101
|
|
|
95
102
|
return request_body
|
|
96
103
|
|
|
104
|
+
def editor_annotation_to_request_body_v2(self, editor_annotation: dict[str, Any], parser: SimpleAnnotationParser) -> dict[str, Any]:
|
|
105
|
+
"""
|
|
106
|
+
`get_editor_annotation`で取得したアノテーション(v2)を、`put_annotation` APIに渡すリクエストボディ(v2)に変換する。
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
editor_annotation: `get_editor_annotation`で取得したアノテーション(v2)
|
|
110
|
+
parser: SimpleAnnotationParserインスタンス。アノテーションのファイルを開くために利用する。
|
|
111
|
+
"""
|
|
112
|
+
request_details: list[dict[str, Any]] = []
|
|
113
|
+
for detail in editor_annotation["details"]:
|
|
114
|
+
new_detail = copy.deepcopy(detail)
|
|
115
|
+
new_detail["_type"] = "Import"
|
|
116
|
+
annotation_id = detail["annotation_id"]
|
|
117
|
+
detail_body_type = detail["body"]["_type"]
|
|
118
|
+
if detail_body_type == "Outer":
|
|
119
|
+
with parser.open_outer_file(annotation_id) as f:
|
|
120
|
+
# TODO content typeを確認
|
|
121
|
+
s3_path = self.service.wrapper.upload_data_to_s3(self.project_id, f, content_type="application/octet-stream")
|
|
122
|
+
new_detail["body"] = {"path": s3_path, "_type": "Outer"}
|
|
123
|
+
request_details.append(new_detail)
|
|
124
|
+
elif detail_body_type == "Inner":
|
|
125
|
+
request_details.append(new_detail)
|
|
126
|
+
else:
|
|
127
|
+
raise ValueError(f"detail_body_type がサポート対象外です。:: detail={detail}")
|
|
128
|
+
|
|
129
|
+
request_body = {"project_id": self.project_id, "task_id": parser.task_id, "input_data_id": parser.input_data_id, "details": request_details, "format_version": "2.0.0"}
|
|
130
|
+
|
|
131
|
+
return request_body
|
|
132
|
+
|
|
97
133
|
def put_annotation_for_input_data(self, parser: SimpleAnnotationParser) -> bool:
|
|
98
134
|
task_id = parser.task_id
|
|
99
135
|
input_data_id = parser.input_data_id
|
|
100
136
|
|
|
101
|
-
old_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id)
|
|
102
|
-
|
|
103
137
|
logger.info(f"task_id='{task_id}', input_data_id='{input_data_id}' :: アノテーションをリストアします。")
|
|
104
|
-
request_body = self.parser_to_request_body(parser)
|
|
105
138
|
|
|
139
|
+
editor_annotation = parser.load_json()
|
|
140
|
+
if editor_annotation.get("format_version") == "2.0.0":
|
|
141
|
+
request_body = self.editor_annotation_to_request_body_v2(editor_annotation, parser)
|
|
142
|
+
else:
|
|
143
|
+
request_body = self.editor_annotation_to_request_body_v1(editor_annotation, parser)
|
|
144
|
+
|
|
145
|
+
old_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id, query_params={"v": "2"})
|
|
106
146
|
updated_datetime = old_annotation["updated_datetime"] if old_annotation is not None else None
|
|
107
147
|
request_body["updated_datetime"] = updated_datetime
|
|
108
148
|
self.service.api.put_annotation(self.project_id, task_id, input_data_id, request_body=request_body)
|
|
@@ -2,6 +2,7 @@ import argparse
|
|
|
2
2
|
from typing import Optional
|
|
3
3
|
|
|
4
4
|
import annofabcli.annotation.change_annotation_attributes
|
|
5
|
+
import annofabcli.annotation.change_annotation_attributes_per_annotation
|
|
5
6
|
import annofabcli.annotation.change_annotation_properties
|
|
6
7
|
import annofabcli.annotation.copy_annotation
|
|
7
8
|
import annofabcli.annotation.delete_annotation
|
|
@@ -21,6 +22,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
21
22
|
|
|
22
23
|
# サブコマンドの定義
|
|
23
24
|
annofabcli.annotation.change_annotation_attributes.add_parser(subparsers)
|
|
25
|
+
annofabcli.annotation.change_annotation_attributes_per_annotation.add_parser(subparsers)
|
|
24
26
|
annofabcli.annotation.change_annotation_properties.add_parser(subparsers)
|
|
25
27
|
annofabcli.annotation.copy_annotation.add_parser(subparsers)
|
|
26
28
|
annofabcli.annotation.delete_annotation.add_parser(subparsers)
|
|
@@ -67,7 +67,7 @@ class PutCommentMain(CommandLineWithConfirm):
|
|
|
67
67
|
"""batch_update_comments に渡すリクエストボディを作成する。"""
|
|
68
68
|
|
|
69
69
|
def _create_dict_annotation_id() -> dict[str, str]:
|
|
70
|
-
content, _ = self.service.api.get_editor_annotation(self.project_id, task["task_id"], input_data_id)
|
|
70
|
+
content, _ = self.service.api.get_editor_annotation(self.project_id, task["task_id"], input_data_id, query_params={"v": "2"})
|
|
71
71
|
details = content["details"]
|
|
72
72
|
return {e["annotation_id"]: e["label_id"] for e in details}
|
|
73
73
|
|
annofabcli/common/visualize.py
CHANGED
|
@@ -375,13 +375,10 @@ class AddProps:
|
|
|
375
375
|
|
|
376
376
|
* user_id
|
|
377
377
|
* username
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
task:
|
|
381
|
-
|
|
382
|
-
Returns:
|
|
383
|
-
Task情報
|
|
378
|
+
* request.user_id
|
|
379
|
+
* request.username
|
|
384
380
|
|
|
385
381
|
"""
|
|
386
382
|
self._add_user_info(task_history_event)
|
|
383
|
+
self._add_user_info(task_history_event["request"])
|
|
387
384
|
return task_history_event
|
|
@@ -81,16 +81,34 @@ class ListTaskHistoryEventWithJson(CommandLine):
|
|
|
81
81
|
main_obj = ListTaskHistoryEventWithJsonMain(self.service)
|
|
82
82
|
task_history_event_list = main_obj.get_task_history_event_list(project_id, task_history_event_json=task_history_event_json, task_id_list=task_id_list)
|
|
83
83
|
|
|
84
|
-
logger.debug(f"
|
|
85
|
-
|
|
86
|
-
if
|
|
87
|
-
|
|
84
|
+
logger.debug(f"{len(task_history_event_list)} 件のタスク履歴イベントの情報を出力します。")
|
|
85
|
+
|
|
86
|
+
if arg_format == FormatArgument.CSV:
|
|
87
|
+
columns = [
|
|
88
|
+
"project_id",
|
|
89
|
+
"task_id",
|
|
90
|
+
"task_history_id",
|
|
91
|
+
"created_datetime",
|
|
92
|
+
"phase",
|
|
93
|
+
"phase_stage",
|
|
94
|
+
"status",
|
|
95
|
+
"account_id",
|
|
96
|
+
"request.status",
|
|
97
|
+
"request.account_id",
|
|
98
|
+
"request.user_id",
|
|
99
|
+
"request.username",
|
|
100
|
+
"request.last_updated_datetime",
|
|
101
|
+
"request.force",
|
|
102
|
+
]
|
|
103
|
+
if len(task_history_event_list) > 0:
|
|
88
104
|
df = pandas.json_normalize(task_history_event_list)
|
|
89
|
-
|
|
105
|
+
df = df[columns]
|
|
90
106
|
else:
|
|
91
|
-
|
|
107
|
+
df = pandas.DataFrame(columns=columns)
|
|
108
|
+
|
|
109
|
+
self.print_csv(df)
|
|
92
110
|
else:
|
|
93
|
-
|
|
111
|
+
self.print_according_to_format(task_history_event_list)
|
|
94
112
|
|
|
95
113
|
def main(self) -> None:
|
|
96
114
|
args = self.args
|
|
@@ -36,6 +36,20 @@ class SimpleTaskHistoryEvent(DataClassJsonMixin):
|
|
|
36
36
|
status: str
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
@dataclass
|
|
40
|
+
class RequestOfTaskHistoryEvent(DataClassJsonMixin):
|
|
41
|
+
"""operateTask APIによってタスク履歴イベントが生成されたときのリクエストボディ
|
|
42
|
+
|
|
43
|
+
ただし、CLIユーザーにとって不要な情報は除いています。
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
status: str
|
|
47
|
+
force: bool
|
|
48
|
+
account_id: Optional[str]
|
|
49
|
+
user_id: Optional[str]
|
|
50
|
+
username: Optional[str]
|
|
51
|
+
|
|
52
|
+
|
|
39
53
|
@dataclass
|
|
40
54
|
class WorktimeFromTaskHistoryEvent(DataClassJsonMixin):
|
|
41
55
|
project_id: str
|
|
@@ -48,6 +62,8 @@ class WorktimeFromTaskHistoryEvent(DataClassJsonMixin):
|
|
|
48
62
|
worktime_hour: float
|
|
49
63
|
start_event: SimpleTaskHistoryEvent
|
|
50
64
|
end_event: SimpleTaskHistoryEvent
|
|
65
|
+
end_event_request: RequestOfTaskHistoryEvent
|
|
66
|
+
"""operateTask APIによってタスク履歴イベントが生成されたときのリクエストボディ"""
|
|
51
67
|
|
|
52
68
|
|
|
53
69
|
class ListWorktimeFromTaskHistoryEventMain:
|
|
@@ -123,6 +139,15 @@ class ListWorktimeFromTaskHistoryEventMain:
|
|
|
123
139
|
user_id = None
|
|
124
140
|
username = None
|
|
125
141
|
|
|
142
|
+
end_event_request_account_id = end_event["request"]["account_id"]
|
|
143
|
+
end_event_request_member = self.visualize.get_project_member_from_account_id(end_event_request_account_id)
|
|
144
|
+
if end_event_request_member is not None:
|
|
145
|
+
end_event_request_user_id = end_event_request_member["user_id"]
|
|
146
|
+
end_event_request_username = end_event_request_member["username"]
|
|
147
|
+
else:
|
|
148
|
+
end_event_request_user_id = None
|
|
149
|
+
end_event_request_username = None
|
|
150
|
+
|
|
126
151
|
return WorktimeFromTaskHistoryEvent(
|
|
127
152
|
# start_eventとend_eventの以下の属性は同じなので、start_eventの値を参照する
|
|
128
153
|
project_id=start_event["project_id"],
|
|
@@ -143,9 +168,16 @@ class ListWorktimeFromTaskHistoryEventMain:
|
|
|
143
168
|
created_datetime=end_event["created_datetime"],
|
|
144
169
|
status=end_event["status"],
|
|
145
170
|
),
|
|
171
|
+
end_event_request=RequestOfTaskHistoryEvent(
|
|
172
|
+
status=end_event["request"]["status"],
|
|
173
|
+
force=end_event["request"]["force"],
|
|
174
|
+
account_id=end_event["request"]["account_id"],
|
|
175
|
+
user_id=end_event_request_user_id,
|
|
176
|
+
username=end_event_request_username,
|
|
177
|
+
),
|
|
146
178
|
)
|
|
147
179
|
|
|
148
|
-
def _create_worktime_list(self, task_history_event_list: list[TaskHistoryEvent]) -> list[WorktimeFromTaskHistoryEvent]:
|
|
180
|
+
def _create_worktime_list(self, task_id: str, task_history_event_list: list[TaskHistoryEvent]) -> list[WorktimeFromTaskHistoryEvent]:
|
|
149
181
|
"""タスク履歴イベントから、作業時間のリストを生成する。
|
|
150
182
|
|
|
151
183
|
Args:
|
|
@@ -174,7 +206,12 @@ class ListWorktimeFromTaskHistoryEventMain:
|
|
|
174
206
|
TaskStatus.ON_HOLD.value,
|
|
175
207
|
TaskStatus.COMPLETE.value,
|
|
176
208
|
}:
|
|
177
|
-
logger.warning(
|
|
209
|
+
logger.warning(
|
|
210
|
+
f"task_id='{task_id}' :: 作業開始のイベント(task_history_id='{event['task_history_id']}')の次のイベント(task_history_id='{next_event['task_history_id']}')は、"
|
|
211
|
+
f"作業終了のイベントではないため、作業時間を算出できません。スキップします。"
|
|
212
|
+
f"タスク履歴イベントが不整合な状態なので、Annofabチームに問い合わせてください。 :: "
|
|
213
|
+
f"start_event='{start_event}', next_event='{next_event}'"
|
|
214
|
+
)
|
|
178
215
|
i += 1
|
|
179
216
|
continue
|
|
180
217
|
|
|
@@ -204,8 +241,8 @@ class ListWorktimeFromTaskHistoryEventMain:
|
|
|
204
241
|
task_history_event_dict = self._create_task_history_event_dict(all_task_history_event_list, task_ids=task_id_set, account_ids=account_id_set)
|
|
205
242
|
|
|
206
243
|
worktime_list = []
|
|
207
|
-
for subset_event_list in task_history_event_dict.
|
|
208
|
-
subset_worktime_list = self._create_worktime_list(subset_event_list)
|
|
244
|
+
for task_id, subset_event_list in task_history_event_dict.items():
|
|
245
|
+
subset_worktime_list = self._create_worktime_list(task_id, subset_event_list)
|
|
209
246
|
worktime_list.extend(subset_worktime_list)
|
|
210
247
|
return worktime_list
|
|
211
248
|
|
|
@@ -230,16 +267,38 @@ class ListWorktimeFromTaskHistoryEvent(CommandLine):
|
|
|
230
267
|
)
|
|
231
268
|
|
|
232
269
|
logger.debug(f"作業時間一覧の件数: {len(worktime_list)}")
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
270
|
+
dict_worktime_list = [e.to_dict() for e in worktime_list]
|
|
271
|
+
|
|
272
|
+
if arg_format == FormatArgument.CSV:
|
|
273
|
+
columns = [
|
|
274
|
+
"project_id",
|
|
275
|
+
"task_id",
|
|
276
|
+
"phase",
|
|
277
|
+
"phase_stage",
|
|
278
|
+
"account_id",
|
|
279
|
+
"user_id",
|
|
280
|
+
"username",
|
|
281
|
+
"worktime_hour",
|
|
282
|
+
"start_event.task_history_id",
|
|
283
|
+
"start_event.created_datetime",
|
|
284
|
+
"start_event.status",
|
|
285
|
+
"end_event.task_history_id",
|
|
286
|
+
"end_event.created_datetime",
|
|
287
|
+
"end_event.status",
|
|
288
|
+
"end_event_request.status",
|
|
289
|
+
"end_event_request.force",
|
|
290
|
+
"end_event_request.account_id",
|
|
291
|
+
"end_event_request.user_id",
|
|
292
|
+
"end_event_request.username",
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
if len(dict_worktime_list) > 0:
|
|
296
|
+
df = pandas.json_normalize(dict_worktime_list)[columns]
|
|
239
297
|
else:
|
|
240
|
-
|
|
298
|
+
df = pandas.DataFrame(columns=columns)
|
|
299
|
+
self.print_csv(df)
|
|
241
300
|
else:
|
|
242
|
-
|
|
301
|
+
self.print_according_to_format(dict_worktime_list)
|
|
243
302
|
|
|
244
303
|
def main(self) -> None:
|
|
245
304
|
args = self.args
|
|
@@ -2,20 +2,21 @@ annofabcli/__init__.py,sha256=fdBtxy5rOI8zi26jf0hmXS5KTBjQIsm2b9ZUSAIR558,319
|
|
|
2
2
|
annofabcli/__main__.py,sha256=83jLGTlNGoXaYU3fz9akYXoL3kgfIuaVXo_TkpK72IU,5267
|
|
3
3
|
annofabcli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
annofabcli/annotation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
annofabcli/annotation/annotation_query.py,sha256=
|
|
5
|
+
annofabcli/annotation/annotation_query.py,sha256=VwfPWpLOpVa2SeEJ264LmCKkBGDJvpX8o7GbWIrDE0o,15712
|
|
6
6
|
annofabcli/annotation/change_annotation_attributes.py,sha256=Zjqax-sb7ujJnTUVv4io0GFmZzfqx0rpN2N1W86uVTs,17092
|
|
7
|
+
annofabcli/annotation/change_annotation_attributes_per_annotation.py,sha256=4OkFOasRpW86akmgZOdKwnt__b-Q6uCwILInxgiy5o8,13878
|
|
7
8
|
annofabcli/annotation/change_annotation_properties.py,sha256=Kp_LZ5sSoVmmjGE80ABVO3InxsXBIxiFFvVcIJNsOMk,18309
|
|
8
9
|
annofabcli/annotation/copy_annotation.py,sha256=Pih2k3vvpgfT3Ovb3gZw2L_8fK_ws_wKR7ARYG5hG_8,18407
|
|
9
|
-
annofabcli/annotation/delete_annotation.py,sha256=
|
|
10
|
+
annofabcli/annotation/delete_annotation.py,sha256=hQApNrx2Ci1bBWk0dRGA0oJkIgDHwl6Jy0-33gYF6jo,22989
|
|
10
11
|
annofabcli/annotation/download_annotation_zip.py,sha256=P_ZpdqIaSFEmB8jjpdykcRhh2tVlHxSlXFrYreJjShE,3282
|
|
11
|
-
annofabcli/annotation/dump_annotation.py,sha256=
|
|
12
|
+
annofabcli/annotation/dump_annotation.py,sha256=CJ10zJUm9C1M1VhB_ZtdkWPncrZTx9h9JyxmLn7f3lo,8397
|
|
12
13
|
annofabcli/annotation/import_annotation.py,sha256=39w-LwhNrT4-20NR5Hy-cn_cUFVVWVnMp9x1d8aITn0,33369
|
|
13
14
|
annofabcli/annotation/list_annotation.py,sha256=uKcOuGC7lzd6vVbzizkiZtYdXJ7EzY0iifuiqKl2wQM,10707
|
|
14
15
|
annofabcli/annotation/list_annotation_count.py,sha256=T9fbaoxWeDJIVgW_YgHRldbwrVZWiE-57lfJrDQrj80,6474
|
|
15
16
|
annofabcli/annotation/merge_segmentation.py,sha256=kIsCeXtJxzd6nobQPpi0fscaRDlTx3tg1qpy5PDfSJI,18107
|
|
16
17
|
annofabcli/annotation/remove_segmentation_overlap.py,sha256=JeeBY3PUcBqLYiNItEH8Ae9zhEy6cf4LldNr_4yBdjY,15991
|
|
17
|
-
annofabcli/annotation/restore_annotation.py,sha256=
|
|
18
|
-
annofabcli/annotation/subcommand_annotation.py,sha256=
|
|
18
|
+
annofabcli/annotation/restore_annotation.py,sha256=F0_p1k0Yzx0YVOuLYOS48OWutsN5ysOu3TRa3yhRdTw,17071
|
|
19
|
+
annofabcli/annotation/subcommand_annotation.py,sha256=PfiU4G_Col3quCXKbZpqWoUXGi5p2uB1XI9BKMwaq1I,2288
|
|
19
20
|
annofabcli/annotation_specs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
21
|
annofabcli/annotation_specs/add_attribute_restriction.py,sha256=jVA7ltharabR94bf71QuW0x0yVgu6-A7PHn7Dq3kUfg,6825
|
|
21
22
|
annofabcli/annotation_specs/attribute_restriction.py,sha256=tXcCLWVUdmmLWFHLjtSERcgxwFhqk7_zyKZzps0oyso,10123
|
|
@@ -37,7 +38,7 @@ annofabcli/comment/delete_comment.py,sha256=NpQVrXrIPI3vjAyOZ-5Mm-Pae_uCHu8BXVEA
|
|
|
37
38
|
annofabcli/comment/download_comment_json.py,sha256=YfqUnMgvLgVFV7FJPIXwirREkQ2E63fXeCaF4hfwk8c,2338
|
|
38
39
|
annofabcli/comment/list_all_comment.py,sha256=zMZSmVe8P89WDBZE5PZn5SIv5j2OTbnP1kS-HnCxPCI,6483
|
|
39
40
|
annofabcli/comment/list_comment.py,sha256=ikXOtfI9W88w7YC1GQQ1ryq2BatHelIRb8Pa5-fKbSs,6187
|
|
40
|
-
annofabcli/comment/put_comment.py,sha256=
|
|
41
|
+
annofabcli/comment/put_comment.py,sha256=7q_GQW7nKUqzm9tf7mjhiPHfd0lBFLFvvdyF5Cn9854,12025
|
|
41
42
|
annofabcli/comment/put_comment_simply.py,sha256=OwanjmQy5nqqIw-i0Gt9APdEdftM-EuZW4pRLYbIwQM,8171
|
|
42
43
|
annofabcli/comment/put_inspection_comment.py,sha256=TL8o_K9LCLxqMEVPs9wNK_X4jzEiJwjr327lziQbeAM,3803
|
|
43
44
|
annofabcli/comment/put_inspection_comment_simply.py,sha256=WuySt-BV4oPETBIgLwpCIH-hh8XRetIHPb6Gtnpgs7A,6805
|
|
@@ -58,7 +59,7 @@ annofabcli/common/pandas.py,sha256=IW9xqHkdRF1I6YZc7CP_9tkGxJuu1MKEXFILjhaNUU0,4
|
|
|
58
59
|
annofabcli/common/type_util.py,sha256=i3r5pFtRYQwJrYkl1-lVQi8XOePQxTUX_cAHgBTsagM,224
|
|
59
60
|
annofabcli/common/typing.py,sha256=_AcEogoaMbib0esfN2RvHshAZH0oyRb2Ro9-rbn7NJ8,330
|
|
60
61
|
annofabcli/common/utils.py,sha256=Eb4DS5j_EVdZW_YnsvIgjTbgWcjHd560plck2_WgrCI,9545
|
|
61
|
-
annofabcli/common/visualize.py,sha256=
|
|
62
|
+
annofabcli/common/visualize.py,sha256=mBPCA5vfYNbE3I1m5oHRpo-E1hsNH11KswILtYRCOKQ,13650
|
|
62
63
|
annofabcli/common/annofab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
63
64
|
annofabcli/common/annofab/annotation_specs.py,sha256=h-YSnuK0riCLoJMmV-KDP3JS-_JMqx0feSvPAB3xCSI,765
|
|
64
65
|
annofabcli/common/annofab/project.py,sha256=BK96o9OO961yfUvcjuHG1DeJ2bMMEYTS5ujtJCvD_4I,891
|
|
@@ -205,11 +206,11 @@ annofabcli/task_history/list_task_history.py,sha256=izzY9PzgrgJW0O3ftAoSEfqa9fK5
|
|
|
205
206
|
annofabcli/task_history/subcommand_task_history.py,sha256=_CvM1ts60GT15iXmsb4CHOyL170ier-q-p9edVuNLvc,1066
|
|
206
207
|
annofabcli/task_history_event/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
207
208
|
annofabcli/task_history_event/download_task_history_event_json.py,sha256=hQLVbQ0HjdiRpXC9tEt_Py3W_b0mo3eVIa-zp0z3fgU,2434
|
|
208
|
-
annofabcli/task_history_event/list_all_task_history_event.py,sha256=
|
|
209
|
-
annofabcli/task_history_event/list_worktime.py,sha256=
|
|
209
|
+
annofabcli/task_history_event/list_all_task_history_event.py,sha256=EeKMyPUxGwYCFtWQHHW954ZserGm8lUqrwNnV1iX9X4,6830
|
|
210
|
+
annofabcli/task_history_event/list_worktime.py,sha256=Y7Pu5DP7scPf7HPt6CTiTvB1_5_Nfi1bStUIaCpkhII,15507
|
|
210
211
|
annofabcli/task_history_event/subcommand_task_history_event.py,sha256=mJVJoT4RXk4HWnY7-Nrsl4If-gtaIIEXd2z7eFZwM2I,1260
|
|
211
|
-
annofabcli-1.
|
|
212
|
-
annofabcli-1.
|
|
213
|
-
annofabcli-1.
|
|
214
|
-
annofabcli-1.
|
|
215
|
-
annofabcli-1.
|
|
212
|
+
annofabcli-1.105.1.dist-info/METADATA,sha256=1wpj99TzN1VpD56ObB9viB1goGsbs7qUMSUhQAk3DHY,5286
|
|
213
|
+
annofabcli-1.105.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
214
|
+
annofabcli-1.105.1.dist-info/entry_points.txt,sha256=C2uSUc-kkLJpoK_mDL5FEMAdorLEMPfwSf8VBMYnIFM,56
|
|
215
|
+
annofabcli-1.105.1.dist-info/licenses/LICENSE,sha256=pcqWYfxFtxBzhvKp3x9MXNM4xciGb2eFewaRhXUNHlo,1081
|
|
216
|
+
annofabcli-1.105.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|