Skip to content

Commit 26d1bd0

Browse files
committed
feat(input_data): support file scheme in update
1 parent 88367c4 commit 26d1bd0

3 files changed

Lines changed: 107 additions & 8 deletions

File tree

annofabcli/input_data/update_input_data.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
get_json_from_args,
2626
)
2727
from annofabcli.common.facade import AnnofabApiFacade
28+
from annofabcli.common.utils import get_file_scheme_path
2829

2930
logger = logging.getLogger(__name__)
3031

@@ -81,11 +82,18 @@ def update_input_data(
8182
logger.warning(f"{log_prefix}入力データは存在しません。")
8283
return UpdateResult.SKIPPED
8384

85+
file_path = get_file_scheme_path(new_input_data_path) if new_input_data_path is not None else None
86+
if file_path is not None and not Path(file_path).exists():
87+
logger.warning(f"{log_prefix}input_data_path='{new_input_data_path}'にファイルは存在しません。入力データの更新をスキップします。")
88+
return UpdateResult.SKIPPED
89+
8490
# 更新する内容の確認メッセージを作成
8591
changes = []
8692
if new_input_data_name is not None:
8793
changes.append(f"input_data_name='{old_input_data['input_data_name']}'を'{new_input_data_name}'に変更")
88-
if new_input_data_path is not None:
94+
if file_path is not None:
95+
changes.append(f"input_data_pathをローカルファイル'{file_path}'のアップロード結果に変更")
96+
elif new_input_data_path is not None:
8997
changes.append(f"input_data_path='{old_input_data['input_data_path']}'を'{new_input_data_path}'に変更")
9098

9199
if len(changes) == 0:
@@ -96,15 +104,18 @@ def update_input_data(
96104
if not self.confirm_processing(f"{log_prefix}{change_message}しますか?"):
97105
return UpdateResult.SKIPPED
98106

99-
request_body = old_input_data
107+
request_body = dict(old_input_data)
100108
request_body["last_updated_datetime"] = old_input_data["updated_datetime"]
101109

102110
if new_input_data_name is not None:
103111
request_body["input_data_name"] = new_input_data_name
104-
if new_input_data_path is not None:
105-
request_body["input_data_path"] = new_input_data_path
112+
if file_path is not None:
113+
self.service.wrapper.put_input_data_from_file(project_id, input_data_id=input_data_id, file_path=file_path, request_body=request_body)
114+
else:
115+
if new_input_data_path is not None:
116+
request_body["input_data_path"] = new_input_data_path
117+
self.service.api.put_input_data(project_id, input_data_id, request_body=request_body)
106118

107-
self.service.api.put_input_data(project_id, input_data_id, request_body=request_body)
108119
logger.debug(f"{log_prefix} :: 入力データを更新しました。 :: {changes}")
109120
return UpdateResult.SUCCESS
110121

@@ -269,12 +280,12 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
269280
" * ヘッダ行あり, カンマ区切り\n"
270281
" * input_data_id (required)\n"
271282
" * input_data_name (optional)\n"
272-
" * input_data_path (optional)\n"
283+
" * input_data_path (optional): ``file://`` を先頭に付けると、ローカルファイルを入力データに使用します。\n"
273284
"更新しないプロパティは、セルの値を空欄にしてください。\n"
274285
),
275286
)
276287

277-
JSON_SAMPLE = '[{"input_data_id":"id1","input_data_name":"new_name1"},{"input_data_id":"id2","input_data_path":"new_path2"}]' # noqa: N806
288+
JSON_SAMPLE = '[{"input_data_id":"id1","input_data_name":"new_name1"},{"input_data_id":"id2","input_data_path":"file://new_image.jpg"}]' # noqa: N806
278289
file_group.add_argument(
279290
"--json",
280291
type=str,

docs/command_reference/input_data/update.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ CSVのフォーマットは以下の通りです。
2727

2828
input_data_id,Yes,更新対象の入力データID
2929
input_data_name,No,変更後の入力データ名。更新しない場合は空欄
30-
input_data_path,No,変更後の入力データパス。更新しない場合は空欄
30+
input_data_path,No,変更後の入力データパス。更新しない場合は空欄。先頭が ``file://`` の場合、ローカルのファイルを入力データに使用します。
3131

3232

3333
以下はCSVファイルのサンプルです。
@@ -39,6 +39,7 @@ CSVのフォーマットは以下の通りです。
3939
id1,new_name1,
4040
id2,,s3://bucket/new_image.jpg
4141
id3,new_name3,https://example.com/new_image.jpg
42+
id4,,file://new_image.jpg
4243
4344
4445
.. warning::
@@ -47,6 +48,8 @@ CSVのフォーマットは以下の通りです。
4748
プライベートストレージを利用するには、Annofabサポート窓口への問い合わせが必要です。
4849
詳細は https://annofab.readme.io/docs/external-storage を参照してください。
4950

51+
``input_data_path`` の先頭が ``file://`` の場合、指定したローカルファイルをアップロードし、入力データのパスをアップロード後のパスに更新します。
52+
5053

5154

5255
.. code-block::
@@ -78,6 +81,10 @@ JSON文字列を指定する場合
7881
"input_data_id": "id3",
7982
"input_data_name": "new_name3",
8083
"input_data_path": "https://example.com/new_image.jpg"
84+
},
85+
{
86+
"input_data_id": "id4",
87+
"input_data_path": "file://new_image.jpg"
8188
}
8289
]
8390
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from pathlib import Path
2+
from types import SimpleNamespace
3+
from typing import cast
4+
from unittest.mock import Mock
5+
6+
import annofabapi
7+
8+
from annofabcli.input_data.update_input_data import UpdateInputDataMain, UpdateResult
9+
10+
11+
def create_old_input_data() -> dict[str, str]:
12+
return {
13+
"input_data_id": "input1",
14+
"input_data_name": "old_name",
15+
"input_data_path": "s3://bucket/old_image.jpg",
16+
"updated_datetime": "2026-01-01T00:00:00.000+09:00",
17+
}
18+
19+
20+
def create_service() -> tuple[annofabapi.Resource, Mock, Mock]:
21+
api = Mock()
22+
wrapper = Mock()
23+
wrapper.get_input_data_or_none.return_value = create_old_input_data()
24+
service = cast(annofabapi.Resource, SimpleNamespace(api=api, wrapper=wrapper))
25+
return service, api, wrapper
26+
27+
28+
def test_update_input_data_with_url_path() -> None:
29+
service, api, wrapper = create_service()
30+
main_obj = UpdateInputDataMain(service, all_yes=True)
31+
32+
result = main_obj.update_input_data("project1", "input1", new_input_data_path="s3://bucket/new_image.jpg")
33+
34+
assert result == UpdateResult.SUCCESS
35+
api.put_input_data.assert_called_once_with(
36+
"project1",
37+
"input1",
38+
request_body={
39+
"input_data_id": "input1",
40+
"input_data_name": "old_name",
41+
"input_data_path": "s3://bucket/new_image.jpg",
42+
"updated_datetime": "2026-01-01T00:00:00.000+09:00",
43+
"last_updated_datetime": "2026-01-01T00:00:00.000+09:00",
44+
},
45+
)
46+
wrapper.put_input_data_from_file.assert_not_called()
47+
48+
49+
def test_update_input_data_with_file_scheme_path(tmp_path: Path) -> None:
50+
input_data_file = tmp_path / "new_image.jpg"
51+
input_data_file.write_bytes(b"image")
52+
service, api, wrapper = create_service()
53+
main_obj = UpdateInputDataMain(service, all_yes=True)
54+
55+
result = main_obj.update_input_data("project1", "input1", new_input_data_name="new_name", new_input_data_path=f"file://{input_data_file}")
56+
57+
assert result == UpdateResult.SUCCESS
58+
wrapper.put_input_data_from_file.assert_called_once_with(
59+
"project1",
60+
input_data_id="input1",
61+
file_path=str(input_data_file),
62+
request_body={
63+
"input_data_id": "input1",
64+
"input_data_name": "new_name",
65+
"input_data_path": "s3://bucket/old_image.jpg",
66+
"updated_datetime": "2026-01-01T00:00:00.000+09:00",
67+
"last_updated_datetime": "2026-01-01T00:00:00.000+09:00",
68+
},
69+
)
70+
api.put_input_data.assert_not_called()
71+
72+
73+
def test_update_input_data_with_missing_file_scheme_path(tmp_path: Path) -> None:
74+
service, api, wrapper = create_service()
75+
main_obj = UpdateInputDataMain(service, all_yes=True)
76+
77+
result = main_obj.update_input_data("project1", "input1", new_input_data_path=f"file://{tmp_path / 'missing.jpg'}")
78+
79+
assert result == UpdateResult.SKIPPED
80+
wrapper.put_input_data_from_file.assert_not_called()
81+
api.put_input_data.assert_not_called()

0 commit comments

Comments
 (0)