Skip to content

Commit

Permalink
Merge pull request #88 from kurusugawa-computer/feature/78
Browse files Browse the repository at this point in the history
[input_data put] zipファイルの登録、ローカルファイルの登録
  • Loading branch information
yuji38kwmt authored Oct 16, 2019
2 parents 5067428 + 20fda49 commit 3656788
Show file tree
Hide file tree
Showing 12 changed files with 237 additions and 32 deletions.
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ $ docker run -it -e ANNOFAB_USER_ID=XXXX -e ANNOFAB_PASSWORD=YYYYY annofab-cli a
|filesystem| write_annotation_image | アノテーションzip、またはそれを展開したディレクトリから、アノテーションの画像(Semantic Segmentation用)を生成します。 |-|
|input_data|delete | 入力データを削除します。 |オーナ|
|input_data|list | 入力データ一覧を出力します。 |-|
|input_data|put | CSVに記載された入力データを登録します|オーナ|
|input_data|put | 入力データを登録します|オーナ|
|inspection_comment| list | 検査コメントを出力します。 |-|
|inspection_comment| list_unprocessed | 未処置の検査コメントを出力します。 |-|
|instruction| upload | HTMLファイルを作業ガイドとして登録します。 |チェッカー/オーナ|
Expand Down Expand Up @@ -434,7 +434,7 @@ $ annofabcli input_data delete --project_id prj1 --input_data_list input1 input2
# input_data_nameが"sample"の入力データ一覧を出力する
$ annofabcli input_data list --project_id prj1 --input_data_query '{"input_data_name": "sample"}'
# 入力データの詳細情報も出力する
# 入力データの詳細情報(参照されているタスクのtask_id `parent_task_id_list`)も出力する
$ annofabcli input_data list --project_id prj1 --input_data_query '{"input_data_name": "sample"}' --add_details
# 段階的に入力データ一覧を取得する。
Expand All @@ -455,12 +455,14 @@ $ annofabcli input_data list --project_id prj1 --batch \


### input_data put
CSVに記載された入力データを登録します。CSVは以下のフォーマットに従います。
CSVに記載された入力データ情報やzipファイルを、入力データとして登録します。

#### CSVに記載された入力データ情報を、入力データとして登録

* ヘッダ行なし
* カンマ区切り
* 1列目: input_data_name. 必須
* 2列目: input_data_path. 必須
* 2列目: input_data_path. 必須. 先頭が`file://`の場合、ローカルのファイルを入力データとしてアップロードします。
* 3列目: input_data_id. 省略可能。省略した場合UUIDv4になる。
* 4列目: sign_required. 省略可能. `true` or `false`

Expand All @@ -471,6 +473,8 @@ data1,s3://example.com/data1,id1,
data2,s3://example.com/data2,id2,true
data3,s3://example.com/data3,id3,false
data4,https://example.com/data4,,
data5,file://sample.jpg,,
data6,file:///tmp/sample.jpg,,
```


Expand All @@ -484,6 +488,7 @@ $ annofabcli input_data put --project_id prj1 --csv input_data.csv --overwrite
```



`input_data list`コマンドを使えば、プロジェクトに既に登録されている入力データからCSVを作成できます。

```
Expand All @@ -493,6 +498,23 @@ $ annofabcli input_data list --project_id prj1 --input_data_query '{"input_data_
```


#### zipファイルを入力データとして登録


```
# 画像や動画が格納されたinput_data.zipを、入力データとして登録する
$ annofabcli input_data put --project_id prj1 --zip input_data.zip
# zipファイルを入力データとして登録し、入力データの登録が完了するまで待つ。
$ annofabcli input_data put --project_id prj1 --zip input_data.zip --wait
# zipファイルを入力データとして登録する。そのときinput_data_nameを`foo.zip`にする。
$ annofabcli input_data put --project_id prj1 --zip input_data.zip --input_data_name_for_zip foo.zip
```




### inspection_comment list
検査コメント一覧を出力します。
Expand Down
2 changes: 1 addition & 1 deletion annofabcli/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.10.0'
__version__ = '1.10.1'
8 changes: 4 additions & 4 deletions annofabcli/common/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ def get_list_from_args(str_list: Optional[List[str]] = None) -> List[str]:
return str_list

str_value = str_list[0]
if str_value.startswith('file://'):
path = str_value[len('file://'):]
path = annofabcli.utils.get_file_scheme_path(str_value)
if path is not None:
return annofabcli.utils.read_lines_except_blank_line(path)
else:
return str_list
Expand Down Expand Up @@ -133,8 +133,8 @@ def get_json_from_args(target: Optional[str] = None) -> Any:
if target is None:
return None

if target.startswith('file://'):
path = target[len('file://'):]
path = annofabcli.utils.get_file_scheme_path(target)
if path is not None:
with open(path, encoding="utf-8") as f:
return json.load(f)
else:
Expand Down
19 changes: 19 additions & 0 deletions annofabcli/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,25 @@ def to_filename(s: str):
return re.sub(r'[\\|/|:|?|.|"|<|>|\|]', '__', s)


def is_file_scheme(str_value: str) -> bool:
"""
file schemaかどうか
"""
return str_value.startswith('file://')


def get_file_scheme_path(str_value: str) -> Optional[str]:
"""
file schemaのパスを取得する。file schemeでない場合は、Noneを返す
"""
if is_file_scheme(str_value):
return str_value[len('file://'):]
else:
return None


def isoduration_to_hour(duration):
"""
ISO 8601 duration を 時間に変換する
Expand Down
153 changes: 134 additions & 19 deletions annofabcli/input_data/put_input_data.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import argparse
import logging
import sys
import uuid
import zipfile
from dataclasses import dataclass
from distutils.util import strtobool # pylint: disable=import-error,no-name-in-module
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple, Union # pylint: disable=unused-import

import pandas
import requests
from annofabapi.models import ProjectMemberRole
from annofabapi.models import JobType, ProjectMemberRole
from dataclasses_json import dataclass_json

import annofabcli
from annofabcli import AnnofabApiFacade
from annofabcli.common.cli import AbstractCommandLineInterface, ArgumentParser, build_annofabapi_resource_and_login
from annofabcli.common.utils import get_file_scheme_path, is_file_scheme

logger = logging.getLogger(__name__)

Expand All @@ -36,15 +39,26 @@ class PutInputData(AbstractCommandLineInterface):
"""
def put_input_data(self, project_id: str, csv_input_data: CsvInputData,
last_updated_datetime: Optional[str] = None):
request_body = {
'input_data_name': csv_input_data.input_data_name,
'input_data_path': csv_input_data.input_data_path,
'sign_required': csv_input_data.sign_required,
}
if last_updated_datetime is not None:
request_body.update({'last_updated_datetime': last_updated_datetime})

self.service.api.put_input_data(project_id, csv_input_data.input_data_id, request_body=request_body)
if is_file_scheme(csv_input_data.input_data_path):
request_body = {
'input_data_name': csv_input_data.input_data_name,
'sign_required': csv_input_data.sign_required,
}
file_path = get_file_scheme_path(csv_input_data.input_data_path)
logger.debug(f"'{file_path}'を入力データとして登録します。")
self.service.wrapper.put_input_data_from_file(project_id, input_data_id=csv_input_data.input_data_id,
file_path=file_path, request_body=request_body)

else:
request_body = {
'input_data_name': csv_input_data.input_data_name,
'input_data_path': csv_input_data.input_data_path,
'sign_required': csv_input_data.sign_required,
}
if last_updated_datetime is not None:
request_body.update({'last_updated_datetime': last_updated_datetime})

self.service.api.put_input_data(project_id, csv_input_data.input_data_id, request_body=request_body)

def confirm_put_input_data(self, csv_input_data: CsvInputData, alread_exists: bool = False) -> bool:

Expand All @@ -69,7 +83,6 @@ def put_input_data_list(self, project_id: str, input_data_list: List[CsvInputDat
"""

super().validate_project(project_id, [ProjectMemberRole.OWNER])
project_title = self.facade.get_project_title(project_id)
logger.info(f"{project_title} に、{len(input_data_list)} 件の入力データを登録します。")

Expand All @@ -79,7 +92,9 @@ def put_input_data_list(self, project_id: str, input_data_list: List[CsvInputDat

last_updated_datetime = None
input_data_id = csv_input_data.input_data_id
input_data_path = csv_input_data.input_data_path
input_data = self.get_input_data(project_id, input_data_id)

if input_data is not None:
if overwrite:
logger.debug(f"input_data_id={input_data_id} はすでに存在します。")
Expand All @@ -88,6 +103,13 @@ def put_input_data_list(self, project_id: str, input_data_list: List[CsvInputDat
logger.debug(f"input_data_id={input_data_id} がすでに存在するのでスキップします。")
continue

file_path = get_file_scheme_path(input_data_path)
logger.debug(f"csv_input_data={csv_input_data}")
if file_path is not None:
if not Path(file_path).exists():
logger.warning(f"{input_data_path} は存在しません。")
continue

if not self.confirm_put_input_data(csv_input_data, alread_exists=(last_updated_datetime is not None)):
continue

Expand All @@ -111,7 +133,7 @@ def put_input_data_list(self, project_id: str, input_data_list: List[CsvInputDat
def get_input_data_list_from_csv(csv_path: Path) -> List[CsvInputData]:
def create_input_data(e):
input_data_id = e.input_data_id if not pandas.isna(e.input_data_id) else str(uuid.uuid4())
sign_required = strtobool(str(e.sign_required)) if not pandas.isna(e.sign_required) else None
sign_required = bool(strtobool(str(e.sign_required))) if not pandas.isna(e.sign_required) else None
return CsvInputData(input_data_name=e.input_data_name, input_data_path=e.input_data_path,
input_data_id=input_data_id, sign_required=sign_required)

Expand All @@ -120,10 +142,90 @@ def create_input_data(e):
input_data_list = [create_input_data(e) for e in df.itertuples()]
return input_data_list

def put_input_data_from_zip_file(self, project_id: str, zip_file: Path,
input_data_name_for_zip: Optional[str] = None, wait: bool = False) -> None:
"""
zipファイルを入力データとして登録する
Args:
project_id: 入力データの登録先プロジェクトのプロジェクトID
zip_file: 入力データとして登録するzipファイルのパス
input_data_name_for_zip: zipファイルのinput_data_name
wait: 入力データの登録が完了するまで待つかどうか
"""

project_title = self.facade.get_project_title(project_id)
logger.info(f"{project_title} に、{str(zip_file)} を登録します。")

request_body = {}
if input_data_name_for_zip is not None:
request_body["input_data_name"] = input_data_name_for_zip

self.service.wrapper.put_input_data_from_file(project_id, input_data_id=str(uuid.uuid4()),
file_path=str(zip_file), content_type="application/zip",
request_body=request_body)
logger.info(f"入力データの登録中です(サーバ側の処理)。")

if wait:
MAX_JOB_ACCESS = 60
JOB_ACCESS_INTERVAL = 60
MAX_WAIT_MINUTU = MAX_JOB_ACCESS * JOB_ACCESS_INTERVAL / 60

result = self.service.wrapper.wait_for_completion(project_id, job_type=JobType.GEN_INPUTS,
job_access_interval=JOB_ACCESS_INTERVAL,
max_job_access=MAX_JOB_ACCESS)
if result:
logger.info(f"入力データの登録が完了しました。")
else:
logger.warning(f"入力データの登録に失敗しました。または、{MAX_WAIT_MINUTU}分間待っても、入力データの登録が完了しませんでした。")

@staticmethod
def validate(args: argparse.Namespace) -> bool:
COMMON_MESSAGE = "annofabcli input_data put: error:"
if args.zip is not None:
if not Path(args.zip).exists():
print(f"{COMMON_MESSAGE} argument --zip: ファイルパスが存在しません。 '{args.zip}'", file=sys.stderr)
return False

if not zipfile.is_zipfile(args.zip):
print(f"{COMMON_MESSAGE} argument --zip: zipファイルではありません。 '{args.zip}'", file=sys.stderr)
return False

if args.overwrite:
logger.warning(f"`--zip`オプションを指定しているとき、`--overwrite`オプションは無視されます。")

if args.csv is not None:
if not Path(args.csv).exists():
print(f"{COMMON_MESSAGE} argument --csv: ファイルパスが存在しません。 '{args.csv}'", file=sys.stderr)
return False

if args.wait:
logger.warning(f"`--csv`オプションを指定しているとき、`--wait`オプションは無視されます。")

if args.input_data_name_for_zip:
logger.warning(f"`--csv`オプションを指定しているとき、`--input_data_name_for_zip`オプションは無視されます。")

return True

def main(self):
args = self.args
input_data_list = self.get_input_data_list_from_csv(Path(args.csv))
self.put_input_data_list(args.project_id, input_data_list=input_data_list, overwrite=args.overwrite)
if not self.validate(args):
return

project_id = args.project_id
super().validate_project(project_id, [ProjectMemberRole.OWNER])

if args.csv is not None:
input_data_list = self.get_input_data_list_from_csv(Path(args.csv))
self.put_input_data_list(project_id, input_data_list=input_data_list, overwrite=args.overwrite)

elif args.zip is not None:
self.put_input_data_from_zip_file(project_id, zip_file=Path(args.zip),
input_data_name_for_zip=args.input_data_name_for_zip, wait=args.wait)

else:
print(f"引数が不正です。", file=sys.stderr)


def main(args):
Expand All @@ -137,24 +239,37 @@ def parse_args(parser: argparse.ArgumentParser):

argument_parser.add_project_id()

parser.add_argument(
'--csv', type=str, required=True,
file_group = parser.add_mutually_exclusive_group(required=True)
file_group.add_argument(
'--csv', type=str,
help=('入力データが記載されたCVファイルのパスを指定してください。'
'CSVのフォーマットは、「1列目:input_data_name(required), 2列目:input_data_path(required), 3列目:input_data_id, '
'4列目:sign_required(bool), ヘッダ行なし, カンマ区切り」です。'
'input_data_pathの先頭が`file://`の場合、ローカルのファイルを入力データとして登録します。 '
'input_data_idが空の場合はUUIDv4になります。'
'各項目の詳細は `putInputData` API を参照してください。'))

file_group.add_argument('--zip', type=str, help=('入力データとして登録するzipファイルのパスを指定してください。'))

parser.add_argument('--overwrite', action='store_true',
help='指定した場合、input_data_idがすでに存在していたら上書きします。指定しなければ、スキップします。')
help='指定した場合、input_data_idがすでに存在していたら上書きします。指定しなければ、スキップします。'
'`--csv`を指定したときのみ有効なオプションです。')

parser.add_argument(
'--input_data_name_for_zip', type=str,
help='入力データとして登録するzipファイルのinput_data_nameを指定してください。省略した場合、`--zip`のパスになります。'
'`--zip`を指定したときのみ有効なオプションです。')

parser.add_argument('--wait', action='store_true', help=("入力データの登録が完了するまで待ちます。最大60分間待ちます。"
"`--zip`を指定したときのみ有効なオプションです。"))

parser.set_defaults(subcommand_func=main)


def add_parser(subparsers: argparse._SubParsersAction):
subcommand_name = "put"
subcommand_help = "CSVに記載された入力データを登録します。"
description = ("CSVに記載された入力データを登録します。")
subcommand_help = "入力データを登録します。"
description = ("CSVに記載された入力データ情報やzipファイルを、入力データとして登録します。")
epilog = "オーナロールを持つユーザで実行してください。"

parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description, epilog=epilog)
Expand Down
3 changes: 3 additions & 0 deletions tests/data/arg.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"foo": 1
}
3 changes: 3 additions & 0 deletions tests/data/arg_id_list.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
id1
id2
id3
1 change: 1 addition & 0 deletions tests/data/input_data2.csv
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
test,https://localhost/sample.jpg,,
test_name,file://tests/data/lenna.png,test_id,false
File renamed without changes
Loading

0 comments on commit 3656788

Please sign in to comment.