Skip to content

Commit 74ba93c

Browse files
Merge pull request #110 from Project-MONAI/kavink/packager_minor_fixes
Add requirements.txt handling, utilize spinner class, and base image verification
2 parents 480b57e + 12fd3a9 commit 74ba93c

File tree

5 files changed

+95
-38
lines changed

5 files changed

+95
-38
lines changed

monai/deploy/packager/constants.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,11 @@ class DefaultValues:
1616
"""
1717

1818
DOCKER_FILE_NAME = "dockerfile"
19-
# TODO(KavinKrishnan): Decide a default image to use.
20-
# BASE_IMAGE = "nvcr.io/nvidia/cuda:11.1-runtime-ubuntu20.04"
2119
BASE_IMAGE = "nvcr.io/nvidia/pytorch:21.07-py3"
22-
WORK_DIR = "/var/monai"
20+
DOCKERFILE_TYPE = "pytorch"
21+
WORK_DIR = "/var/monai/"
2322
INPUT_DIR = "input"
2423
OUTPUT_DIR = "output"
2524
MODELS_DIR = "/opt/monai/models"
2625
API_VERSION = "0.1.0"
27-
VERSION = "0.0.0"
2826
TIMEOUT = 0

monai/deploy/packager/package_command.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,21 @@ def create_package_parser(subparser: _SubParsersAction, command: str, parents: L
2323

2424
parser.add_argument("application", type=str, help="MONAI application path")
2525
parser.add_argument("--tag", "-t", required=True, type=str, help="MONAI application package tag")
26-
parser.add_argument("--base", type=str, help="Base Application Image")
27-
parser.add_argument("--working-dir", "-w", type=str, help="Directory mounted in container for Application")
26+
parser.add_argument("--base", "-b", type=str, help="Base Application Image")
2827
parser.add_argument("--input-dir", "-i", type=str, help="Directory mounted in container for Application Input")
29-
parser.add_argument("--output-dir", "-o", type=str, help="Directory mounted in container for Application Output")
3028
parser.add_argument("--models-dir", type=str, help="Directory mounted in container for Models Path")
3129
parser.add_argument("--model", "-m", type=str, help="Optional Path to directory containing all application models")
32-
parser.add_argument("--version", type=str, help="Version of the Application")
30+
parser.add_argument("--no-cache", "-n", action="store_true", help="Packager will not use cache when building image")
31+
parser.add_argument("--output-dir", "-o", type=str, help="Directory mounted in container for Application Output")
32+
parser.add_argument("--working-dir", "-w", type=str, help="Directory mounted in container for Application")
33+
parser.add_argument(
34+
"--requirements",
35+
"-r",
36+
type=str,
37+
help="Optional Path to requirements.txt containing package dependencies of application",
38+
)
3339
parser.add_argument("--timeout", type=str, help="Timeout")
40+
parser.add_argument("--version", type=str, help="Version of the Application")
3441

3542
return parser
3643

monai/deploy/packager/util.py

Lines changed: 79 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,36 @@
2424
from monai.deploy.packager.templates import Template
2525
from monai.deploy.utils.fileutil import checksum
2626
from monai.deploy.utils.importutil import dist_module_path, dist_requires, get_application
27+
from monai.deploy.utils.spinner import ProgressSpinner
2728

2829
logger = logging.getLogger("app_packager")
2930

3031
executor_url = "https://globalcdn.nuget.org/packages/monai.deploy.executor.0.1.0-prealpha.0.nupkg"
3132

3233

34+
def verify_base_image(base_image: str) -> str:
35+
"""Helper function which validates whether valid base image passed to Packager.
36+
Additionally, this function provides the string identifier of the dockerfile
37+
template to build MAP
38+
Args:
39+
base_image (str): potential base image to build MAP docker image
40+
Returns:
41+
str: returns string identifier of the dockerfile template to build MAP
42+
if valid base image provided, returns empty string otherwise
43+
"""
44+
valid_prefixes = {"nvcr.io/nvidia/cuda": "ubuntu", "nvcr.io/nvidia/pytorch": "pytorch"}
45+
46+
for prefix, template in valid_prefixes.items():
47+
if prefix in base_image:
48+
return template
49+
50+
return ""
51+
52+
3353
def initialize_args(args: Namespace) -> Dict:
3454
"""Processes and formats input arguements for Packager
35-
3655
Args:
3756
args (Namespace): Input arguements for Packager from CLI
38-
3957
Returns:
4058
Dict: Processed set of input arguements for Packager
4159
"""
@@ -45,16 +63,49 @@ def initialize_args(args: Namespace) -> Dict:
4563
processed_args["application"] = args.application
4664
processed_args["tag"] = args.tag
4765
processed_args["docker_file_name"] = DefaultValues.DOCKER_FILE_NAME
48-
processed_args["base_image"] = args.base if args.base else DefaultValues.BASE_IMAGE
4966
processed_args["working_dir"] = args.working_dir if args.working_dir else DefaultValues.WORK_DIR
5067
processed_args["app_dir"] = "/opt/monai/app"
5168
processed_args["executor_dir"] = "/opt/monai/executor"
5269
processed_args["input_dir"] = args.input if args.input_dir else DefaultValues.INPUT_DIR
5370
processed_args["output_dir"] = args.output if args.output_dir else DefaultValues.OUTPUT_DIR
5471
processed_args["models_dir"] = args.models if args.models_dir else DefaultValues.MODELS_DIR
55-
processed_args["api-version"] = DefaultValues.API_VERSION
72+
processed_args["no_cache"] = args.no_cache
5673
processed_args["timeout"] = args.timeout if args.timeout else DefaultValues.TIMEOUT
57-
processed_args["version"] = args.version if args.version else DefaultValues.VERSION
74+
processed_args["api-version"] = DefaultValues.API_VERSION
75+
processed_args["requirements"] = ""
76+
77+
if args.requirements:
78+
if not args.requirements.endswith(".txt"):
79+
logger.error(
80+
f"Improper path to requirements.txt provided: {args.requirements}, defaulting to sdk provided values"
81+
)
82+
else:
83+
processed_args["requirements"] = args.requirements
84+
85+
# Verify proper base image:
86+
dockerfile_type = ""
87+
88+
if args.base:
89+
dockerfile_type = verify_base_image(args.base)
90+
if not dockerfile_type:
91+
logger.error(
92+
"Provided base image '{}' is not supported \n \
93+
Please provide a Cuda or Pytorch image from https://ngc.nvidia.com/ (nvcr.io/nvidia)".format(
94+
args.base
95+
)
96+
)
97+
sys.exit(1)
98+
99+
processed_args["dockerfile_type"] = dockerfile_type if args.base else DefaultValues.DOCKERFILE_TYPE
100+
101+
base_image = ""
102+
if args.base:
103+
base_image = args.base
104+
elif os.getenv("MONAI_BASEIMAGE"):
105+
base_image = os.getenv("MONAI_BASEIMAGE")
106+
else:
107+
base_image = DefaultValues.BASE_IMAGE
108+
processed_args["base_image"] = base_image
58109

59110
# Obtain SDK provide application values
60111
app_obj = get_application(args.application)
@@ -63,12 +114,14 @@ def initialize_args(args: Namespace) -> Dict:
63114
else:
64115
raise WrongValueError("Application from '{}' not found".format(args.application))
65116

117+
# Use version number if provided through CLI, otherwise use value provided by SDK
118+
processed_args["version"] = args.version if args.version else processed_args["application_info"]["app-version"]
119+
66120
return processed_args
67121

68122

69123
def build_image(args: dict, temp_dir: str):
70124
"""Creates dockerfile and builds MONAI Application Package (MAP) image
71-
72125
Args:
73126
args (dict): Input arguements for Packager
74127
temp_dir (str): Temporary directory to build MAP
@@ -77,6 +130,7 @@ def build_image(args: dict, temp_dir: str):
77130
tag = args["tag"]
78131
docker_file_name = args["docker_file_name"]
79132
base_image = args["base_image"]
133+
dockerfile_type = args["dockerfile_type"]
80134
working_dir = args["working_dir"]
81135
app_dir = args["app_dir"]
82136
executor_dir = args["executor_dir"]
@@ -87,6 +141,9 @@ def build_image(args: dict, temp_dir: str):
87141
models_dir = args["models_dir"]
88142
timeout = args["timeout"]
89143
application_path = args["application"]
144+
local_requirements_file = args["requirements"]
145+
no_cache = args["no_cache"]
146+
app_version = args["version"]
90147

91148
# Copy application files to temp directory (under 'app' folder)
92149
target_application_path = os.path.join(temp_dir, "app")
@@ -96,13 +153,12 @@ def build_image(args: dict, temp_dir: str):
96153
else:
97154
shutil.copytree(application_path, target_application_path)
98155

99-
# Copy monai-deploy-app-sdk module to temp directory (under 'monai-deploy-app-sdk' folder)
156+
# Copy monai-app-sdk module to temp directory (under 'monai-deploy-app-sdk' folder)
100157
monai_app_sdk_path = os.path.join(dist_module_path("monai-deploy-app-sdk"), "monai", "deploy")
101158
target_monai_app_sdk_path = os.path.join(temp_dir, "monai-deploy-app-sdk")
102159
shutil.copytree(monai_app_sdk_path, target_monai_app_sdk_path)
103160

104161
# Parse SDK provided values
105-
app_version = args["application_info"]["app-version"]
106162
sdk_version = args["application_info"]["sdk-version"]
107163
local_models = args["application_info"]["models"]
108164
pip_packages = args["application_info"]["pip-packages"]
@@ -115,7 +171,13 @@ def build_image(args: dict, temp_dir: str):
115171
os.makedirs(pip_folder, exist_ok=True)
116172
pip_requirements_path = os.path.join(pip_folder, "requirements.txt")
117173
with open(pip_requirements_path, "w") as requirements_file:
118-
requirements_file.writelines("\n".join(pip_packages))
174+
# Use local requirements.txt packages if provided, otherwise use sdk provided packages
175+
if local_requirements_file:
176+
with open(local_requirements_file, "r") as lr:
177+
for line in lr:
178+
requirements_file.write(line)
179+
else:
180+
requirements_file.writelines("\n".join(pip_packages))
119181
map_requirements_path = "/tmp/requirements.txt"
120182

121183
# Copy model files to temp directory (under 'model' folder)
@@ -153,7 +215,7 @@ def build_image(args: dict, temp_dir: str):
153215
"timeout": timeout,
154216
"working_dir": working_dir,
155217
}
156-
docker_template_string = Template.get_template("pytorch").format(**template_params)
218+
docker_template_string = Template.get_template(dockerfile_type).format(**template_params)
157219

158220
# Write out dockerfile
159221
logger.debug(docker_template_string)
@@ -169,34 +231,25 @@ def build_image(args: dict, temp_dir: str):
169231

170232
# Build dockerfile into an MAP image
171233
docker_build_cmd = ["docker", "build", "-f", docker_file_path, "-t", tag, temp_dir]
234+
if no_cache:
235+
docker_build_cmd.append("--no-cache")
172236
proc = subprocess.Popen(docker_build_cmd, stdout=subprocess.PIPE)
173237

174-
def build_spinning_wheel():
175-
while True:
176-
for cursor in "|/-\\":
177-
yield cursor
178-
179-
spinner = build_spinning_wheel()
180-
181-
print("Building MONAI Application Package... ")
238+
spinner = ProgressSpinner("Building MONAI Application Package... ")
239+
spinner.start()
182240

183241
while proc.poll() is None:
184-
if proc.stdout:
185-
logger.debug(proc.stdout.readline().decode("utf-8"))
186-
sys.stdout.write(next(spinner))
187-
sys.stdout.flush()
188-
sys.stdout.write("\b")
189-
sys.stdout.write("\b")
242+
logger.debug(proc.stdout.readline().decode("utf-8"))
190243

244+
spinner.stop()
191245
return_code = proc.returncode
192246

193247
if return_code == 0:
194-
print(f"Successfully built {tag}")
248+
logger.info(f"Successfully built {tag}")
195249

196250

197251
def create_app_manifest(args: Dict, temp_dir: str):
198252
"""Creates Application manifest .json file
199-
200253
Args:
201254
args (Dict): Input arguements for Packager
202255
temp_dir (str): Temporary directory to build MAP
@@ -237,7 +290,6 @@ def create_app_manifest(args: Dict, temp_dir: str):
237290

238291
def create_package_manifest(args: Dict, temp_dir: str):
239292
"""Creates package manifest .json file
240-
241293
Args:
242294
args (Dict): Input arguements for Packager
243295
temp_dir (str): Temporary directory to build MAP
@@ -284,7 +336,6 @@ def create_package_manifest(args: Dict, temp_dir: str):
284336
def package_application(args: Namespace):
285337
"""Driver function for invoking all functions for creating and
286338
building the MONAI Application package image
287-
288339
Args:
289340
args (Namespace): Input arguements for Packager from CLI
290341
"""

monai/deploy/utils/argparse_types.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,3 @@ def valid_existing_path(path: str) -> Path:
7777
if file_path.exists():
7878
return file_path
7979
raise argparse.ArgumentTypeError(f"No such file/folder: '{file_path}'")
80-

monai/deploy/utils/spinner.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class ProgressSpinner:
2020
"""
2121

2222
def __init__(self, message, delay=0.2):
23-
self.spinner_symbols = itertools.cycle(["-", "/", "|", "\\"])
23+
self.spinner_symbols = itertools.cycle(["-", "\\", "|", "/"])
2424
self.delay = delay
2525
self.stop_event = Event()
2626
self.spinner_visible = False
@@ -69,6 +69,8 @@ def stop(self):
6969
"""
7070
Stop spinner process.
7171
"""
72+
sys.stdout.write("\b")
73+
sys.stdout.write("Done")
7274
if sys.stdout.isatty():
7375
self.stop_event.set()
7476
self._remove_spinner(cleanup=True)

0 commit comments

Comments
 (0)