Skip to content

Commit 7619cdb

Browse files
vykozlovalvarolopez
authored andcommitted
Adds deepaas-cli command to use API methods from the command line
Added files: add cli.py, deepaas-cli.rst Modified: setup.cfg
1 parent a7fcc4b commit 7619cdb

File tree

3 files changed

+430
-0
lines changed

3 files changed

+430
-0
lines changed

deepaas/cmd/cli.py

Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
#!/usr/bin/env python -*- coding: utf-8 -*-
2+
3+
# Copyright 2020 Spanish National Research Council (CSIC)
4+
# Copyright (c) 2018 - 2020 Karlsruhe Institute of Technology - SCC
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
7+
# not use this file except in compliance with the License. You may obtain
8+
# a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15+
# License for the specific language governing permissions and limitations
16+
# under the License.
17+
18+
19+
# import asyncio
20+
import deepaas
21+
import json
22+
import mimetypes
23+
import multiprocessing as mp
24+
import os
25+
import re
26+
import shutil
27+
import sys
28+
import tempfile
29+
import time
30+
31+
from oslo_config import cfg
32+
from oslo_log import log
33+
34+
# from deepaas import config
35+
from deepaas.model import loading
36+
from deepaas.model.v2 import wrapper as v2_wrapper
37+
38+
39+
debug_cli = False
40+
41+
42+
# Helper function to get subdictionary from dict_one based on keys in dict_two
43+
def _get_subdict(dict_one, dict_two):
44+
"""Function to get subdictionary from dict_one based on keys in dict_two
45+
:param dict_one: dict to select subdictionary from
46+
:param dict_two: dict, its keys are used to select entries from dict_one
47+
:return: selected subdictionary
48+
"""
49+
sub_dict = {k: dict_one[k] for k in dict_two.keys() if k in dict_one}
50+
return sub_dict
51+
52+
53+
# Convert mashmallow fields to dict()
54+
def _fields_to_dict(fields_in):
55+
"""Function to convert mashmallow fields to dict()
56+
:param fields_in: mashmallow fields
57+
:return: python dictionary
58+
"""
59+
60+
dict_out = {}
61+
62+
for key, val in fields_in.items():
63+
param = {}
64+
param['default'] = val.missing
65+
param['type'] = type(val.missing)
66+
if key == 'files' or key == 'urls':
67+
param['type'] = str
68+
69+
val_help = val.metadata['description']
70+
# argparse hates % sign:
71+
if '%' in val_help:
72+
# replace single occurancies of '%' with '%%'
73+
# since '%%' is accepted by argparse
74+
val_help = re.sub(r'(?<!%)%(?!%)', r'%%', val_help)
75+
76+
if 'enum' in val.metadata.keys():
77+
val_help = "{}. Choices: {}".format(val_help,
78+
val.metadata['enum'])
79+
param['help'] = val_help
80+
81+
try:
82+
val_req = val.required
83+
except Exception:
84+
val_req = False
85+
param['required'] = val_req
86+
87+
dict_out[key] = param
88+
return dict_out
89+
90+
91+
# Function to get a model object
92+
def _get_model_name(model_name=None):
93+
"""Function to get model_obj from the name of the model.
94+
In case of error, prints the list of available models
95+
:param model_name: name of the model
96+
:return: model object
97+
"""
98+
99+
models = loading.get_available_models("v2")
100+
if model_name:
101+
model_obj = models.get(model_name)
102+
if model_obj is None:
103+
sys.stderr.write(
104+
"[ERROR]: The model {} is not available.\n"
105+
"Available models: {}\n".format(model_name,
106+
list(models.keys())))
107+
sys.exit(1)
108+
109+
return model_name, model_obj
110+
111+
elif len(models) == 1:
112+
return models.popitem()
113+
114+
else:
115+
sys.stderr.write(
116+
'[ERROR]: There are several models available ({}).\n'
117+
'You have to choose one and set it in the DEEPAAS_V2_MODEL '
118+
'environment setting.\n'.format(list(models.keys())))
119+
sys.exit(1)
120+
121+
122+
# Get the model name
123+
model_name = None
124+
if 'DEEPAAS_V2_MODEL' in os.environ:
125+
model_name = os.environ['DEEPAAS_V2_MODEL']
126+
127+
model_name, model_obj = _get_model_name(model_name)
128+
129+
# use deepaas.model.v2.wrapper.ModelWrapper(). deepaas>1.2.1dev4
130+
# model_obj = v2_wrapper.ModelWrapper(name=model_name,
131+
# model_obj=model_obj)
132+
133+
134+
# Once we know the model name,
135+
# we get arguments for predict and train as dictionaries
136+
predict_args = _fields_to_dict(model_obj.get_predict_args())
137+
train_args = _fields_to_dict(model_obj.get_train_args())
138+
139+
140+
# Function to add later these arguments to CONF via SubCommandOpt
141+
def _add_methods(subparsers):
142+
"""Function to add argparse subparsers via SubCommandOpt (see below)
143+
for DEEPaaS methods get_metadata, warm, predict, train
144+
"""
145+
146+
# in case no method requested, we return get_metadata(). check main()
147+
subparsers.required = False
148+
149+
get_metadata_parser = subparsers.add_parser('get_metadata', # noqa: F841
150+
help='get_metadata method')
151+
152+
get_warm_parser = subparsers.add_parser('warm', # noqa: F841
153+
help='warm method, e.g. to '
154+
'prepare the model for execution')
155+
156+
# get predict arguments configured
157+
predict_parser = subparsers.add_parser('predict',
158+
help='predict method, use '
159+
'predict --help for the full list')
160+
161+
for key, val in predict_args.items():
162+
predict_parser.add_argument('--%s' % key,
163+
default=val['default'],
164+
type=val['type'],
165+
help=val['help'],
166+
required=val['required'])
167+
# get train arguments configured
168+
train_parser = subparsers.add_parser('train',
169+
help='train method, use '
170+
'train --help for the full list')
171+
172+
for key, val in train_args.items():
173+
train_parser.add_argument('--%s' % key,
174+
default=val['default'],
175+
type=val['type'],
176+
help=val['help'],
177+
required=val['required'])
178+
179+
180+
# Now options to be registered with oslo_config
181+
cli_opts = [
182+
# intentionally long to avoid a conflict with opts from predict, train etc
183+
cfg.StrOpt('deepaas_method_output',
184+
help="Define an output file, if needed",
185+
deprecated_name='deepaas_model_output'),
186+
cfg.BoolOpt('deepaas_with_multiprocessing',
187+
default=True,
188+
help='Activate multiprocessing; default is True'),
189+
cfg.SubCommandOpt('methods',
190+
title='methods',
191+
handler=_add_methods,
192+
help='Use \"<method> --help\" to get '
193+
'more info about options for '
194+
'each method')
195+
]
196+
197+
198+
CONF = cfg.CONF
199+
CONF.register_cli_opts(cli_opts)
200+
201+
LOG = log.getLogger(__name__)
202+
203+
204+
# store DEEPAAS_METHOD output in a file
205+
def _store_output(results, out_file):
206+
"""Function to store model results in the file
207+
:param results: what to store (JSON expected)
208+
:param out_file: in what file to store
209+
"""
210+
211+
out_path = os.path.dirname(os.path.abspath(out_file))
212+
if not os.path.exists(out_path): # Create path if does not exist
213+
os.makedirs(out_path)
214+
215+
f = open(out_file, "w+")
216+
f.write(results)
217+
f.close()
218+
219+
LOG.info("[INFO, Output] Output is saved in {}".format(out_file))
220+
221+
222+
# async def main():
223+
def main():
224+
"""Executes model's methods with corresponding parameters"""
225+
226+
# we may add deepaas config, but then too many options...
227+
# config.config_and_logging(sys.argv)
228+
229+
log.register_options(CONF)
230+
log.set_defaults(default_log_levels=log.get_default_log_levels())
231+
232+
CONF(sys.argv[1:],
233+
project='deepaas',
234+
version=deepaas.__version__)
235+
236+
log.setup(CONF, "deepaas-cli")
237+
238+
LOG.info("[INFO, Method] {} was called.".format(CONF.methods.name))
239+
240+
# put all variables in dict, makes life easier...
241+
conf_vars = vars(CONF._namespace)
242+
243+
if CONF.deepaas_with_multiprocessing:
244+
mp.set_start_method('spawn', force=True)
245+
246+
# TODO(multi-file): change to many files ('for' itteration)
247+
if CONF.methods.__contains__('files'):
248+
if CONF.methods.files:
249+
# create tmp file as later it supposed
250+
# to be deleted by the application
251+
temp = tempfile.NamedTemporaryFile()
252+
temp.close()
253+
# copy original file into tmp file
254+
with open(conf_vars['files'], "rb") as f:
255+
with open(temp.name, "wb") as f_tmp:
256+
for line in f:
257+
f_tmp.write(line)
258+
259+
# create file object
260+
file_type = mimetypes.MimeTypes().guess_type(conf_vars['files'])[0]
261+
file_obj = v2_wrapper.UploadedFile(
262+
name="data", filename=temp.name,
263+
content_type=file_type, original_filename=conf_vars['files'])
264+
# re-write 'files' parameter in conf_vars
265+
conf_vars['files'] = file_obj
266+
267+
# debug of input parameters
268+
LOG.debug("[DEBUG provided options, conf_vars]: {}".format(conf_vars))
269+
270+
if CONF.methods.name == 'get_metadata':
271+
meta = model_obj.get_metadata()
272+
meta_json = json.dumps(meta)
273+
LOG.debug("[DEBUG, get_metadata, Output]: {}".format(meta_json))
274+
if CONF.deepaas_method_output:
275+
_store_output(meta_json, CONF.deepaas_method_output)
276+
277+
return meta_json
278+
279+
elif CONF.methods.name == 'warm':
280+
# await model_obj.warm()
281+
model_obj.warm()
282+
LOG.info("[INFO, warm] Finished warm() method")
283+
284+
elif CONF.methods.name == 'predict':
285+
# call predict method
286+
predict_vars = _get_subdict(conf_vars, predict_args)
287+
task = model_obj.predict(**predict_vars)
288+
289+
if CONF.deepaas_method_output:
290+
out_file = CONF.deepaas_method_output
291+
out_path = os.path.dirname(os.path.abspath(out_file))
292+
if not os.path.exists(out_path): # Create path if does not exist
293+
os.makedirs(out_path)
294+
# check extension of the output file
295+
out_filename, out_extension = os.path.splitext(out_file)
296+
297+
# set default extension for the data returned
298+
# by the application to .json
299+
extension = ".json"
300+
# check what is asked to return by the application (if --accept)
301+
if CONF.methods.__contains__('accept'):
302+
if CONF.methods.accept:
303+
extension = mimetypes.guess_extension(CONF.methods.accept)
304+
305+
if (extension is not None and out_extension is not None
306+
and extension != out_extension): # noqa: W503
307+
out_file = out_file + extension
308+
LOG.warn("[WARNING] You are trying to store {} "
309+
"type data in the file "
310+
"with {} extension!\n"
311+
"===================> "
312+
"New output is {}".format(extension,
313+
out_extension,
314+
out_file))
315+
if extension == ".json" or extension is None:
316+
results_json = json.dumps(task)
317+
LOG.debug("[DEBUG, predict, Output]: {}".format(results_json))
318+
f = open(out_file, "w+")
319+
f.write(results_json)
320+
f.close()
321+
else:
322+
out_results = task.name
323+
shutil.copy(out_results, out_file)
324+
325+
LOG.info("[INFO, Output] Output is saved in {}".format(out_file))
326+
327+
return task
328+
329+
elif CONF.methods.name == 'train':
330+
train_vars = _get_subdict(conf_vars, train_args)
331+
start = time.time()
332+
task = model_obj.train(**train_vars)
333+
LOG.info("[INFO] Elapsed time: %s", str(time.time() - start))
334+
# we assume that train always returns JSON
335+
results_json = json.dumps(task)
336+
LOG.debug("[DEBUG, train, Output]: {}".format(results_json))
337+
if CONF.deepaas_method_output:
338+
_store_output(results_json, CONF.deepaas_method_output)
339+
340+
return results_json
341+
342+
else:
343+
LOG.warn("[WARNING] No Method was requested! Return get_metadata()")
344+
meta = model_obj.get_metadata()
345+
meta_json = json.dumps(meta)
346+
LOG.debug("[DEBUG, get_metadata, Output]: {}".format(meta_json))
347+
348+
return meta_json
349+
350+
351+
if __name__ == '__main__':
352+
# loop = asyncio.get_event_loop()
353+
# loop.run_until_complete(main())
354+
# loop.close()
355+
main()

0 commit comments

Comments
 (0)