Skip to content

Commit fa56355

Browse files
authored
Merge pull request #96 from ImagingDataCommons/add-smart-download
enh: add zero parameter download cli
2 parents 965730b + 3338aa9 commit fa56355

3 files changed

Lines changed: 102 additions & 11 deletions

File tree

idc_index/cli.py

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from __future__ import annotations
66

77
import logging
8+
from pathlib import Path
89

910
import click
1011

@@ -14,7 +15,7 @@
1415
# Set up logging for the CLI module
1516
logging.basicConfig(format="%(asctime)s - %(message)s", level=logging.DEBUG)
1617
logger_cli = logging.getLogger("cli")
17-
logger_cli.setLevel("WARNING")
18+
logger_cli.setLevel(logging.INFO)
1819

1920

2021
@click.group()
@@ -38,6 +39,7 @@ def set_log_level(log_level):
3839
logging_level = log_levels.get(log_level.lower(), logging.WARNING)
3940
logger_cli.debug(f"Setting the log level of index.py to {logging_level}")
4041
index.logger.setLevel(logging_level)
42+
logger_cli.setLevel(logging_level)
4143

4244

4345
@main.command()
@@ -128,6 +130,7 @@ def download_from_selection(
128130
set_log_level(log_level)
129131
# Create an instance of the IDCClient
130132
client = IDCClient()
133+
logger_cli.info(f"Downloading from IDC {client.get_idc_version()} index")
131134
# Parse the input parameters and pass them to IDCClient's download_from_selection method
132135
collection_id = (
133136
[cid.strip() for cid in (",".join(collection_id)).split(",")]
@@ -236,6 +239,7 @@ def download_from_manifest(
236239
set_log_level(log_level)
237240
# Create an instance of the IDCClient
238241
client = IDCClient()
242+
logger_cli.info(f"Downloading from IDC {client.get_idc_version()} index")
239243
logger_cli.debug("Inputs received from cli manifest download:")
240244
logger_cli.debug(f"manifest_file_path: {manifest_file}")
241245
logger_cli.debug(f"download_dir: {download_dir}")
@@ -253,5 +257,81 @@ def download_from_manifest(
253257
)
254258

255259

260+
@main.command()
261+
@click.argument(
262+
"generic_argument",
263+
type=str,
264+
)
265+
@click.option(
266+
"--log-level",
267+
type=click.Choice(
268+
["debug", "info", "warning", "error", "critical"], case_sensitive=False
269+
),
270+
default="info",
271+
help="Set the logging level for the CLI module.",
272+
)
273+
def download(generic_argument, log_level):
274+
"""Download content given the input parameter.
275+
276+
Determine whether the input parameter corresponds to a file manifest or a list of collection_id, PatientID, StudyInstanceUID, or SeriesInstanceUID values, and download the corresponding files into the current directory. Default parameters will be used for organizing the downloaded files into folder hierarchy. Use `download_from_selection()` and `download_from_manifest()` functions if granular control over the download process is needed.
277+
"""
278+
# Set the logging level for the CLI module
279+
set_log_level(log_level)
280+
# Create an instance of the IDCClient
281+
client = IDCClient()
282+
283+
logger_cli.info(f"Downloading from IDC {client.get_idc_version()} index")
284+
285+
download_dir = Path.cwd()
286+
287+
if Path(generic_argument).is_file():
288+
# Parse the input parameters and pass them to IDC
289+
logger_cli.info("Detected manifest file, downloading from manifest.")
290+
client.download_from_manifest(generic_argument, downloadDir=download_dir)
291+
# this is not a file manifest
292+
else:
293+
# Split the input string and filter out any empty values
294+
item_ids = [item for item in generic_argument.split(",") if item]
295+
296+
if not item_ids:
297+
logger_cli.error("No valid IDs provided.")
298+
299+
index_df = client.index
300+
301+
def check_and_download(column_name, item_ids, download_dir, kwarg_name):
302+
matches = index_df[column_name].isin(item_ids)
303+
matched_ids = index_df[column_name][matches].unique().tolist()
304+
if not matched_ids:
305+
return False
306+
unmatched_ids = list(set(item_ids) - set(matched_ids))
307+
if unmatched_ids:
308+
logger_cli.debug(
309+
f"Partial match for {column_name}: matched {matched_ids}, unmatched {unmatched_ids}"
310+
)
311+
logger_cli.info(f"Identified matching {column_name}: {matched_ids}")
312+
client.download_from_selection(
313+
**{kwarg_name: matched_ids, "downloadDir": download_dir}
314+
)
315+
return True
316+
317+
matches_found = 0
318+
matches_found += check_and_download(
319+
"collection_id", item_ids, download_dir, "collection_id"
320+
)
321+
matches_found += check_and_download(
322+
"PatientID", item_ids, download_dir, "patientId"
323+
)
324+
matches_found += check_and_download(
325+
"StudyInstanceUID", item_ids, download_dir, "studyInstanceUID"
326+
)
327+
matches_found += check_and_download(
328+
"SeriesInstanceUID", item_ids, download_dir, "seriesInstanceUID"
329+
)
330+
if not matches_found:
331+
logger_cli.error(
332+
"None of the values passed matched any of the identifiers: collection_id, PatientID, StudyInstanceUID, SeriesInstanceUID."
333+
)
334+
335+
256336
if __name__ == "__main__":
257337
main()

idc_index/index.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -781,10 +781,10 @@ def _track_download_progress(
781781
show_progress_bar: bool = True,
782782
list_of_directories=None,
783783
):
784-
logger.info("Inputs received for tracking download:")
785-
logger.info(f"size_MB: {size_MB}")
786-
logger.info(f"downloadDir: {downloadDir}")
787-
logger.info(f"show_progress_bar: {show_progress_bar}")
784+
logger.debug("Inputs received for tracking download:")
785+
logger.debug(f"size_MB: {size_MB}")
786+
logger.debug(f"downloadDir: {downloadDir}")
787+
logger.debug(f"show_progress_bar: {show_progress_bar}")
788788

789789
runtime_errors = []
790790

@@ -798,7 +798,7 @@ def _track_download_progress(
798798

799799
logger.info("Initial size of the directory: %s bytes", initial_size_bytes)
800800
logger.info(
801-
"Approx. Size of the files need to be downloaded: %s bytes",
801+
"Approximate size of the files that need to be downloaded: %s bytes",
802802
total_size_bytes,
803803
)
804804

@@ -902,7 +902,7 @@ def _parse_s5cmd_sync_output_and_generate_synced_manifest(
902902
sync_size = merged_df["series_size_MB"].sum()
903903
sync_size_rounded = round(sync_size, 2)
904904

905-
logger.info(f"sync_size_rounded: {sync_size_rounded}")
905+
logger.debug(f"sync_size_rounded: {sync_size_rounded}")
906906

907907
if dirTemplate is not None:
908908
hierarchy = self._generate_sql_concat_for_building_directory(
@@ -1050,11 +1050,11 @@ def _s5cmd_run(
10501050
if sync_size < total_size:
10511051
logger.info(
10521052
"""
1053-
Destination folder is not empty and sync size is less than total size. Displaying a warning
1053+
Destination folder is not empty and sync size is less than total size.
10541054
"""
10551055
)
10561056
existing_data_size = round(total_size - sync_size, 2)
1057-
logger.warning(
1057+
logger.info(
10581058
f"Requested total download size is {total_size} MB, \
10591059
however at least {existing_data_size} MB is already present,\
10601060
so downloading only remaining upto {sync_size} MB\n\
@@ -1075,7 +1075,7 @@ def _s5cmd_run(
10751075
)
10761076
else:
10771077
logger.info(
1078-
"Not using s5cmd sync dry run as the destination folder is empty or sync dry or progress bar is not requested"
1078+
"Not using s5cmd sync as the destination folder is empty or sync or progress bar is not requested"
10791079
)
10801080
cmd = [
10811081
self.s5cmdPath,
@@ -1430,7 +1430,7 @@ def download_from_selection(
14301430
list_of_directories = result_df.path.to_list()
14311431
else:
14321432
list_of_directories = [downloadDir]
1433-
logger.info(
1433+
logger.debug(
14341434
"""
14351435
Temporary download manifest is generated and is passed to self._s5cmd_run
14361436
"""

tests/idcindex.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import tempfile
66
import unittest
77
from itertools import product
8+
from pathlib import Path
89

910
import pandas as pd
1011
import pytest
@@ -27,6 +28,7 @@ def setUp(self):
2728
self.client = index.IDCClient()
2829
self.download_from_manifest = cli.download_from_manifest
2930
self.download_from_selection = cli.download_from_selection
31+
self.download = cli.download
3032

3133
logger = logging.getLogger("idc_index")
3234
logger.setLevel(logging.DEBUG)
@@ -421,6 +423,15 @@ def test_cli_download_from_manifest(self):
421423
)
422424
assert len(os.listdir(temp_dir)) != 0
423425

426+
def test_cli_download(self):
427+
runner = CliRunner()
428+
with runner.isolated_filesystem():
429+
result = runner.invoke(
430+
self.download,
431+
["1.3.6.1.4.1.14519.5.2.1.7695.1700.114861588187429958687900856462"],
432+
)
433+
assert len(os.listdir(Path.cwd())) != 0
434+
424435

425436
if __name__ == "__main__":
426437
unittest.main()

0 commit comments

Comments
 (0)