Skip to content

Commit 8863606

Browse files
authored
Support uploading files through selection or glob pattern (#80)
* blank filter is not supported * allow specifying glob pattern or file selection on upload, refactor to use ask * add some glob validation, fix issue if giving a home dir on windows * text change * quotes * output_f -> output_transformer
1 parent 69074ed commit 8863606

File tree

6 files changed

+98
-83
lines changed

6 files changed

+98
-83
lines changed

cirro/api/services/process.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ def list(self, process_type: Executor = None) -> List[Process]:
5555
}
5656
}
5757
'''
58-
item_filter = {}
58+
item_filter = None
5959
if process_type:
60-
item_filter['executor'] = {'eq': process_type.value}
60+
item_filter = {'executor': {'eq': process_type.value}}
6161

6262
items = fetch_all_items(self._api_client, query,
6363
input_variables={'filter': item_filter})

cirro/cli/controller.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,12 @@ def run_ingest(input_params: UploadArguments, interactive=False):
5757

5858
if interactive:
5959
input_params = gather_upload_arguments(input_params, projects, processes)
60-
60+
files = input_params['files']
61+
else:
62+
files = get_files_in_directory(input_params['data_directory'])
63+
if len(files) == 0:
64+
raise RuntimeWarning("No files to upload, exiting")
6165
directory = input_params['data_directory']
62-
files = get_files_in_directory(directory)
63-
if len(files) == 0:
64-
raise RuntimeWarning("No files to upload, exiting")
65-
6666
process = get_item_from_name_or_id(processes, input_params['process'])
6767
cirro.process.check_dataset_files(files, process.id, directory)
6868

Lines changed: 67 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import sys
2+
from fnmatch import fnmatch
23
from pathlib import Path
34
from typing import List
45

56
from prompt_toolkit.shortcuts import CompleteStyle
67
from prompt_toolkit.validation import Validator, ValidationError
8+
from questionary import Choice
79

810
from cirro.api.models.process import Process
911
from cirro.api.models.project import Project
1012
from cirro.cli.interactive.common_args import ask_project
11-
from cirro.cli.interactive.utils import prompt_wrapper
13+
from cirro.cli.interactive.utils import ask
1214
from cirro.cli.models import UploadArguments
13-
from cirro.file_utils import get_directory_stats
15+
from cirro.file_utils import get_directory_stats, get_files_in_directory
1416

1517

1618
class DataDirectoryValidator(Validator):
@@ -23,82 +25,83 @@ def validate(self, document):
2325
)
2426

2527

26-
def ask_data_directory(input_value: str) -> str:
27-
directory_prompt = {
28-
'type': 'path',
29-
'name': 'data_directory',
30-
'message': 'Enter the full path of the data directory',
31-
'validate': DataDirectoryValidator,
32-
'default': input_value or '',
33-
'complete_style': CompleteStyle.READLINE_LIKE,
34-
'only_directories': True
35-
}
28+
def confirm_data_directory(directory: str, files: List[str]):
29+
stats = get_directory_stats(directory, files)
30+
is_accepted = ask(
31+
'confirm',
32+
f'Please confirm that you wish to upload {stats["numberOfFiles"]} files ({stats["sizeFriendly"]})',
33+
default=True
34+
)
3635

37-
answers = prompt_wrapper(directory_prompt)
38-
return answers['data_directory']
39-
40-
41-
def confirm_data_directory(directory: str):
42-
stats = get_directory_stats(directory)
43-
answers = prompt_wrapper({
44-
'type': 'confirm',
45-
'message': f'Please confirm that you wish to upload {stats["numberOfFiles"]} files ({stats["sizeFriendly"]})',
46-
'name': 'continue',
47-
'default': True
48-
})
49-
50-
if not answers['continue']:
36+
if not is_accepted:
5137
sys.exit(1)
5238

5339

54-
def ask_name(input_value: str) -> str:
55-
name_prompt = {
56-
'type': 'input',
57-
'name': 'name',
58-
'message': 'What is the name of this dataset?',
59-
'validate': lambda val: len(val.strip()) > 0 or 'Please enter a name',
60-
'default': input_value or ''
61-
}
62-
63-
answers = prompt_wrapper(name_prompt)
64-
return answers['name']
65-
66-
67-
def ask_description(input_value: str) -> str:
68-
description_prompt = {
69-
'type': 'input',
70-
'name': 'description',
71-
'message': 'Enter a description of the dataset (optional)',
72-
'default': input_value or ''
73-
}
74-
75-
answers = prompt_wrapper(description_prompt)
76-
return answers['description']
77-
78-
7940
def ask_process(processes: List[Process], input_value: str) -> str:
8041
process_names = [process.name for process in processes]
81-
process_prompt = {
82-
'type': 'list',
83-
'name': 'process',
84-
'message': 'What type of files?',
85-
'choices': process_names,
86-
'default': input_value if input_value in process_names else None
87-
}
88-
answers = prompt_wrapper(process_prompt)
89-
return answers['process']
42+
return ask(
43+
'select',
44+
'What type of files?',
45+
default=input_value if input_value in process_names else None,
46+
choices=process_names
47+
)
9048

9149

9250
def gather_upload_arguments(input_params: UploadArguments, projects: List[Project], processes: List[Process]):
9351
input_params['project'] = ask_project(projects, input_params.get('project'))
9452

95-
input_params['data_directory'] = ask_data_directory(input_params.get('data_directory'))
96-
confirm_data_directory(input_params['data_directory'])
53+
input_params['data_directory'] = ask(
54+
'path',
55+
'Enter the full path of the data directory',
56+
required=True,
57+
validate=DataDirectoryValidator,
58+
default=input_params.get('data_directory') or '',
59+
complete_style=CompleteStyle.READLINE_LIKE,
60+
only_directories=True
61+
)
62+
63+
upload_method = ask(
64+
'select',
65+
'What files would you like to upload?',
66+
choices=[
67+
Choice('Upload all files in directory', 'all'),
68+
Choice('Choose files from a list', 'select'),
69+
Choice('Specify a glob pattern', 'glob'),
70+
]
71+
)
72+
input_params['files'] = get_files_in_directory(input_params['data_directory'])
73+
if upload_method == 'select':
74+
input_params['files'] = ask(
75+
'checkbox',
76+
'Select the files you wish to upload',
77+
choices=input_params['files']
78+
)
79+
elif upload_method == 'glob':
80+
matching_files = None
81+
while not matching_files:
82+
glob_pattern = ask('text', 'Glob pattern:')
83+
matching_files = [f for f in input_params['files'] if fnmatch(f, glob_pattern)]
84+
if len(matching_files) == 0:
85+
print('Glob pattern does not match any files, please specify another')
86+
87+
input_params['files'] = matching_files
88+
89+
confirm_data_directory(input_params['data_directory'], input_params['files'])
9790

9891
input_params['process'] = ask_process(processes, input_params.get('process'))
9992

10093
data_directory_name = Path(input_params['data_directory']).name
10194
default_name = input_params.get('name') or data_directory_name
102-
input_params['name'] = ask_name(default_name)
103-
input_params['description'] = ask_description(input_params.get('description'))
95+
input_params['name'] = ask(
96+
'text',
97+
'What is the name of this dataset?',
98+
default=default_name,
99+
validate=lambda val: len(val.strip()) > 0 or 'Please enter a name'
100+
)
101+
input_params['description'] = ask(
102+
'text',
103+
'Enter a description of the dataset (optional)',
104+
default=input_params.get('description') or ''
105+
)
106+
104107
return input_params

cirro/cli/interactive/utils.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import List, Union, Callable
2+
13
import questionary
24
from questionary import prompt
35

@@ -25,16 +27,23 @@ def type_validator(t, v):
2527
return False
2628

2729

28-
def ask(fname, msg, validate_type=None, output_f=None, **kwargs) -> str:
29-
"""Wrap questionary functions to catch escapes and exit gracefully."""
30+
def ask(function_name: str,
31+
msg: str,
32+
validate_type=None,
33+
output_transformer: Callable = None,
34+
**kwargs) -> Union[str, List[str]]:
35+
"""
36+
Wrap questionary functions to catch escapes and exit gracefully.
37+
function_name: https://questionary.readthedocs.io/en/stable/pages/types.html#
38+
"""
3039

3140
# Get the questionary function
32-
questionary_f = questionary.__dict__.get(fname)
41+
questionary_f = questionary.__dict__.get(function_name)
3342

3443
# Make sure that the function exists
35-
assert questionary_f is not None, f"No such questionary function: {fname}"
44+
assert questionary_f is not None, f"No such questionary function: {function_name}"
3645

37-
if fname == "select":
46+
if function_name == "select":
3847
kwargs["use_shortcuts"] = True
3948

4049
if validate_type is not None:
@@ -59,10 +68,9 @@ def ask(fname, msg, validate_type=None, output_f=None, **kwargs) -> str:
5968
raise KeyboardInterrupt()
6069

6170
# If an output transformation function was defined
62-
if output_f is not None:
63-
71+
if output_transformer is not None:
6472
# Call the function
65-
resp = output_f(resp)
73+
resp = output_transformer(resp)
6674

6775
# Otherwise
6876
return resp

cirro/cli/models.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import TypedDict
1+
from typing import TypedDict, List, Optional
22

33

44
class DownloadArguments(TypedDict):
@@ -15,6 +15,7 @@ class UploadArguments(TypedDict):
1515
process: str
1616
data_directory: str
1717
interactive: bool
18+
files: Optional[List[str]]
1819

1920

2021
class ListArguments(TypedDict):

cirro/file_utils.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def _is_hidden_file(file_path: Path):
4040

4141

4242
def get_files_in_directory(directory) -> List[str]:
43-
path = Path(directory)
43+
path = Path(directory).expanduser()
4444
path_posix = str(path.as_posix())
4545

4646
paths = []
@@ -59,8 +59,11 @@ def get_files_in_directory(directory) -> List[str]:
5959
return paths
6060

6161

62-
def get_directory_stats(directory) -> DirectoryStatistics:
63-
sizes = [f.stat().st_size for f in Path(directory).glob('**/*') if f.is_file()]
62+
def get_directory_stats(directory: str, files: List[str] = None) -> DirectoryStatistics:
63+
if files:
64+
sizes = [Path(directory, f).stat().st_size for f in files]
65+
else:
66+
sizes = [f.stat().st_size for f in Path(directory).glob('**/*') if f.is_file()]
6467
total_size = sum(sizes) / float(1 << 30)
6568
return {
6669
'sizeFriendly': f'{total_size:,.3f} GB',

0 commit comments

Comments
 (0)