def plot_all(inputs): """ Plot all of the datapoints for all of the output files using pyNAMEplot :param inputs: running args :return: """ rundir = inputs['outputdir'] # Parse input params into plot options plotoptions = {'station': (inputs['longitude'], inputs['latitude'])} plotoptions['outdir'] = os.path.join(rundir, "plots") files = glob.glob(os.path.join(rundir, 'outputs', '*_group*.txt')) if len(files) == 0: raise Exception( "Unable to find any output files to plot. File names must be named '*_group*.txt'" ) print("Plot options: %s" % plotoptions) for filename in os.listdir(os.path.join(rundir, 'outputs')): if '_group' in filename and filename.endswith('.txt'): n = Name(os.path.join(rundir, 'outputs', filename)) for column in n.timestamps: plotoptions['outfile'] = "name_%s_%s_%s_%sdayback_%sm.png" % ( inputs['title'], filename.split('_')[0], column.split(' ')[1].replace( ':', ''), inputs['time'], inputs['elevation_out']) try: drawMap(n, column, **plotoptions) except: print("Failed to plot %s" % column) return
def run_name(params, response): """ This is the function to actually run NAME :param params: input parameters :param response: the WPS response object :return: names of the output dir and zipped file """ # Remove any unsafe characters params['title'] = clean_title(params['title']) runtype = 'FWD' if params['runBackwards']: runtype = 'BCK' jasconfigs = getjasminconfigs() runtime = datetime.strftime(datetime.now(), '%s') params['runid'] = '{}{}_{}_{}_{}'.format(runtype, params['time'], params['timestamp'], params['title'], runtime) params['outputdir'] = os.path.join(jasconfigs.get('jasmin', 'outputdir'), params['runid']) if not os.path.exists(params['outputdir']): os.makedirs(params['outputdir']) os.makedirs(os.path.join(params['outputdir'], 'inputs')) os.makedirs(os.path.join(params['outputdir'], 'outputs')) os.makedirs(os.path.join(params['outputdir'], 'lotus')) # Will write a file that lists all the input parameters with open(os.path.join(params['outputdir'], 'user_input_parameters.txt'), 'w') as ins: for p in sorted(params): if p == 'outputdir': continue ins.write('%s: %s\n' % (p, params[p])) # Will loop through all the dates in range, including the final day for i, cur_date in enumerate( daterange(params['startdate'], params['enddate'] + timedelta(days=1))): os.makedirs( os.path.join(params['outputdir'], 'met_data', 'input{}'.format(i + 1))) with open( os.path.join(params['outputdir'], 'inputs', 'input{}.txt'.format(i + 1)), 'w') as fout: fout.write(generate_inputfile(params, cur_date, i + 1)) with open(os.path.join(params['outputdir'], 'script.bsub'), 'w') as fout: fout.write(write_file(params, i + 1)) response.update_status('Input files created', 10) cat = subprocess.Popen( ['cat', os.path.join(params['outputdir'], 'script.bsub')], stdout=subprocess.PIPE) runbsub = subprocess.Popen('bsub', stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=cat.stdout) sout, serr = runbsub.communicate() jobid = sout.split(b' ')[1].replace(b'>', b'').replace(b'<', b'') jobrunning = True while jobrunning: time.sleep(30) checkjob = subprocess.check_output('bjobs') if jobid in checkjob: print('Job %s is still running' % jobid) processesrunning = 0 for l in checkjob.split(b'\n'): if jobid in l: processesrunning += 1 percentcomplete = (( (i + 1) - processesrunning) / float(i + 1)) * 85 response.update_status('Running NAME', 10 + percentcomplete) else: jobrunning = False response.update_status('NAME simulation finished', 95) outputs = os.path.join(params['outputdir'], 'outputs') # Sum all of the output files and plot them on one plot s = Sum(outputs) s.sumAll() plot_filename = '{}_{}_summed_all.png'.format(s.runname, s.altitude.strip('()')) plot_path = os.path.join(outputs, plot_filename) drawMap(s, 'total', outfile=plot_path) # Zip all the output files into one directory to be served back to the user zipped_path = os.path.join(params['outputdir'], params['runid']) shutil.make_archive(zipped_path, 'zip', os.path.join(params['outputdir'], 'outputs')) return params['runid'], zipped_path, plot_path
def run_name(params, response): """ This is the function to actually run NAME :param params: input parameters :param response: the WPS response object :return: names of the output dir and zipped file """ # replace any white space in title with underscores params['title'] = params['title'].replace(' ', '_') params['title'] = params['title'].replace(',', '') params['title'] = params['title'].replace('(', '') params['title'] = params['title'].replace(')', '') runtype = "FWD" if params['runBackwards']: runtype = "BCK" jasconfigs = getjasminconfigs() runtime = datetime.strftime(datetime.now(), "%s") params['runid'] = "{}{}_{}_{}_{}".format(runtype, params['time'], params['timestamp'], params['title'], runtime) params['outputdir'] = os.path.join(jasconfigs.get('jasmin', 'outputdir'), params['runid']) if not os.path.exists(params['outputdir']): os.makedirs(params['outputdir']) os.makedirs(os.path.join(params['outputdir'], 'inputs')) os.makedirs(os.path.join(params['outputdir'], 'outputs')) # Will write a file that lists all the input parameters with open(os.path.join(params['outputdir'], 'user_input_parameters.txt'), 'w') as ins: for p in sorted(params): if p == 'outputdir': continue ins.write("%s: %s\n" % (p, params[p])) # Will loop through all the dates in range, including the final day for i, cur_date in enumerate( daterange(params['startdate'], params['enddate'] + timedelta(days=1))): os.makedirs( os.path.join(params['outputdir'], 'met_data', "input{}".format(i + 1))) with open( os.path.join(params['outputdir'], "inputs", "input{}.txt".format(i + 1)), 'w') as fout: fout.write(generate_inputfile(params, cur_date, i + 1)) with open(os.path.join(params['outputdir'], 'script.bsub'), 'w') as fout: fout.write(write_file(params, i + 1)) response.update_status("Input files created", 10) # TODO: We'll insert the commands to run NAME here. # cat = subprocess.Popen(['cat', os.path.join(params['outputdir'], 'script.bsub')], stdout=subprocess.PIPE) # runbsub = subprocess.Popen('bsub', stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=cat.stdout) # sout, serr = runbsub.communicate() # jobid = sout.split(' ')[1].replace('>', '').replace('<', '') # jobrunning = True # while jobrunning: # time.sleep(30) # checkjob = subprocess.check_output('bjobs') # if jobid in checkjob: # print("Job %s is still running" % jobid) # processesrunning = 0 # for l in checkjob.split('\n'): # if jobid in l: # processesrunning += 1 # percentcomplete = (((i+1)-processesrunning)/float(i+1))*85 # response.update_status("Running NAME", 10+percentcomplete) # else: # jobrunning = False response.update_status("NAME simulation finished", 95) # TODO: Need to replace this with an actual result file fakefile = os.path.join(jasconfigs.get('jasmin', 'outputdir'), '20171101_output.txt') n = Name(fakefile) mapfile = "ExamplePlot.png" drawMap(n, n.timestamps[0], outfile=mapfile) # Zip all the output files into one directory to be served back to the user. zippedfile = params['runid'] shutil.make_archive(zippedfile, 'zip', params['outputdir']) return params['runid'], zippedfile, mapfile
def _handler(self, request, response): jasconfigs = getjasminconfigs() rundir = os.path.join(jasconfigs.get('jasmin', 'outputdir'), request.inputs['filelocation'][0].data) LOGGER.debug('Working Directory for plots: %s' % rundir) # Parse NAME run input params inputs = {} with open(os.path.join(rundir, 'user_input_parameters.txt'), 'r') as ins: for l in ins: data = l.rstrip().split(': ') inputs[data[0]] = data[1] # Throw manually with temporary bbox solution if request.inputs['min_lon'][0].data < -180: raise InvalidParameterValue( 'Bounding box minimum longitude input cannot be below -180') if request.inputs['max_lon'][0].data > 180: raise InvalidParameterValue( 'Bounding box maximum longitude input cannot be above 180') if request.inputs['min_lat'][0].data < -90: raise InvalidParameterValue( 'Bounding box minimum latitude input cannot be below -90') if request.inputs['max_lat'][0].data > 90: raise InvalidParameterValue( 'Bounding box minimum latitude input cannot be above 90') # Parse input params into plot options plotoptions = {} # When using bbox: # plotoptions['lon_bounds'] = (int(float(request.inputs['domain'][0].data[1])), int(float(request.inputs['domain'][0].data[3]))) # plotoptions['lat_bounds'] = (int(float(request.inputs['domain'][0].data[0])), int(float(request.inputs['domain'][0].data[2]))) # When using temporary bbox solution plotoptions['lon_bounds'] = (int(request.inputs['min_lon'][0].data), int(request.inputs['max_lon'][0].data)) plotoptions['lat_bounds'] = (int(request.inputs['min_lat'][0].data), int(request.inputs['max_lat'][0].data)) plotoptions['outdir'] = os.path.join( rundir, 'plots_{}'.format(datetime.strftime(datetime.now(), '%s'))) for p in request.inputs: # When using bbox # if p == 'timestamp' or p == 'filelocation' or p == 'summarise' or p == 'domain': # When using temporary bbox solution if p == 'timestamp' or p == 'filelocation' or p == 'summarise' or p == 'min_lon' or p == 'max_lon' or p == 'min_lat' or p == 'max_lat': continue elif p == 'scale': statcoords = request.inputs[p][0].data.split(',') plotoptions[p] = (float(statcoords[0].strip()), float(statcoords[1].strip())) elif p == 'station' and request.inputs[p][0].data == True: plotoptions[p] = (float(inputs['longitude']), float(inputs['latitude'])) else: plotoptions[p] = request.inputs[p][0].data files = glob.glob(os.path.join(rundir, 'outputs', '*_group*.txt')) if len(files) == 0: raise InvalidParameterValue( 'Unable to find any output files. File names must be named "*_group*.txt"' ) if 'timestamp' in request.inputs: request.inputs['summarise'][0].data = 'NA' LOGGER.debug('Plot options: %s' % plotoptions) response.update_status('Processed plot parameters', 5) tot_plots = get_num_dates(start=datetime.date( parse(inputs['startdate'])), end=datetime.date(parse(inputs['enddate'])), sum=request.inputs['summarise'][0].data, type=inputs['timestamp']) # We need to find all the groups and loop through them one at a time! groups = {} for filename in os.listdir(os.path.join(rundir, 'outputs')): if not filename.endswith('txt'): continue groupnum = filename[14] try: groupnum = int(groupnum) except: raise Exception('Cannot identify groupnumber %s' % groupnum) if groupnum in groups: shutil.copy(os.path.join(rundir, 'outputs', filename), groups[groupnum]) else: groups[groupnum] = tempfile.mkdtemp() shutil.copy(os.path.join(rundir, 'outputs', filename), groups[groupnum]) ngroups = len(groups) tot_plots = tot_plots * ngroups plots_made = 0 response.update_status('Plotting', 10) oldper = 10 for groupnum, tmpdir in sorted(groups.items()): if request.inputs['summarise'][0].data != 'NA': s = Sum(tmpdir) if request.inputs['summarise'][0].data == 'week': for week in range(1, 53): s.sumWeek(week) if len(s.files) == 0: LOGGER.debug('No files found for week %s' % week) continue plotoptions[ 'caption'] = '{} {} {} {}: {} week {} sum (UTC)'.format( s.runname, s.averaging, s.altitude, s.direction, s.year, week) plotoptions['outfile'] = '{}_{}_{}_{}_weekly.png'.format( s.runname, s.altitude.strip('()'), s.year, week) try: drawMap(s, 'total', **plotoptions) LOGGER.debug('Plotted %s' % plotoptions['outfile']) except: LOGGER.error('Failed to plot %s' % plotoptions['outfile']) plots_made += 1 newper = 10 + int((plots_made / float(tot_plots)) * 85) if oldper != newper: response.update_status('Plotting', newper) oldper = newper elif request.inputs['summarise'][0].data == 'month': for month in range(1, 13): s.sumMonth(str(month)) if len(s.files) == 0: LOGGER.debug('No files found for month %s' % month) continue plotoptions[ 'caption'] = '{} {} {} {}: {} {} sum (UTC)'.format( s.runname, s.averaging, s.altitude, s.direction, s.year, calendar.month_name[month]) plotoptions['outfile'] = '{}_{}_{}_{}_monthly.png'.format( s.runname, s.altitude.strip('()'), s.year, month) try: drawMap(s, 'total', **plotoptions) LOGGER.debug('Plotted %s' % plotoptions['outfile']) except: LOGGER.error('Failed to plot %s' % plotoptions['outfile']) plots_made += 1 newper = 10 + int((plots_made / float(tot_plots)) * 85) if oldper != newper: response.update_status('Plotting', newper) oldper = newper elif request.inputs['summarise'][0].data == 'all': s.sumAll() plotoptions['caption'] = '{} {} {} {}: Summed (UTC)'.format( s.runname, s.averaging, s.altitude, s.direction) plotoptions['outfile'] = '{}_{}_summed_all.png'.format( s.runname, s.altitude.strip('()')) #TODO: Copy to plot all #TODO: Fix: Currently all levels plotted with same name and overwritten, bug? # Happening because of: https://github.com/TeriForey/pyNAMEplot/blob/master/pynameplot/namereader/name.py#L151 # Is is this correct or an assumption? # If correct then maybe text input should be a string list try: drawMap(s, 'total', **plotoptions) LOGGER.debug('Plotted %s' % plotoptions['outfile']) except: LOGGER.error('Failed to plot %s' % plotoptions['outfile']) plots_made += 1 newper = 10 + int((plots_made / float(tot_plots)) * 85) if oldper != newper: response.update_status('Plotting', newper) oldper = newper else: for filename in os.listdir(tmpdir): if '_group' in filename and filename.endswith('.txt'): if request.inputs['summarise'][0].data == 'day': #s = Sum(tmpdir) date = util.shortname(filename) s.sumDay(date) plotoptions[ 'caption'] = '{} {} {} {}: {}{}{} day sum (UTC)'.format( s.runname, s.averaging, s.altitude, s.direction, s.year, s.month, s.day) plotoptions[ 'outfile'] = '{}_{}_{}{}{}_daily.png'.format( s.runname, s.altitude.strip('()'), s.year, s.month, s.day) try: drawMap(s, 'total', **plotoptions) LOGGER.debug('Plotted %s' % plotoptions['outfile']) except: LOGGER.error('Failed to plot %s' % plotoptions['outfile']) plots_made += 1 newper = 10 + int( (plots_made / float(tot_plots)) * 85) if oldper != newper: response.update_status('Plotting', newper) oldper = newper elif request.inputs['summarise'][0].data == 'NA': n = Name(os.path.join(tmpdir, filename)) if 'timestamp' in request.inputs: timestamp = datetime.strftime( request.inputs['timestamp'][0].data, '%d/%m/%Y %H:%M UTC') LOGGER.debug('Reformatted time: %s' % timestamp) if timestamp in n.timestamps: try: drawMap(n, timestamp, **plotoptions) LOGGER.debug('Plotted %s' % timestamp) except: LOGGER.error('Failed to plot %s' % timestamp) plots_made += 1 newper = 10 + int( (plots_made / float(tot_plots)) * 85) if oldper != newper: response.update_status( 'Plotting', newper) oldper = newper break else: for column in n.timestamps: try: drawMap(n, column, **plotoptions) LOGGER.debug('Plotted %s' % column) except: LOGGER.error('Failed to plot %s' % column) plots_made += 1 newper = 10 + int( (plots_made / float(tot_plots)) * 85) if oldper != newper: response.update_status( 'Plotting', newper) oldper = newper # Finished plotting so will now delete temp directory shutil.rmtree(tmpdir) # Outputting different response based on the number of plots generated response.update_status('Formatting output', 95) if not os.path.exists(plotoptions['outdir']): LOGGER.debug('Did not create any plots') response.outputs['FileContents'].data_format = FORMATS.TEXT response.outputs[ 'FileContents'].data = 'No plots created, check input options' else: if len(os.listdir(plotoptions['outdir'])) == 1: LOGGER.debug('Only one output plot') response.outputs['FileContents'].data_format = Format( 'image/png') response.outputs['FileContents'].file = os.path.join( plotoptions['outdir'], os.listdir(plotoptions['outdir'])[0]) else: zippedfile = '{}_plots'.format( request.inputs['filelocation'][0].data) shutil.make_archive(zippedfile, 'zip', plotoptions['outdir']) LOGGER.debug('Zipped file: %s (%s bytes)' % (zippedfile + '.zip', os.path.getsize(zippedfile + '.zip'))) response.outputs['FileContents'].data_format = FORMATS.SHP response.outputs['FileContents'].file = zippedfile + '.zip' response.update_status('done', 100) return response
def _handler(self, request, response): jasconfigs = getjasminconfigs() rundir = os.path.join(jasconfigs.get('jasmin', 'outputdir'), request.inputs['filelocation'][0].data) LOGGER.debug("Working Directory for plots: %s" % rundir) # Parse NAME run input params inputs = {} with open(os.path.join(rundir, 'user_input_parameters.txt'), 'r') as ins: for l in ins: data = l.rstrip().split(': ') inputs[data[0]] = data[1] # Parse input params into plot options plotoptions = {} plotoptions['outdir'] = os.path.join( rundir, "plots_{}".format(datetime.strftime(datetime.now(), "%s"))) for p in request.inputs: if p == "timestamp" or p == "filelocation" or p == "summarise": continue elif p == 'lon_bounds' or p == 'lat_bounds': statcoords = request.inputs[p][0].data.split(',') plotoptions[p] = (int(statcoords[0].strip()), int(statcoords[1].strip())) elif p == 'scale': statcoords = request.inputs[p][0].data.split(',') plotoptions[p] = (float(statcoords[0].strip()), float(statcoords[1].strip())) elif p == "station" and request.inputs[p][0].data == True: plotoptions[p] = (float(inputs['longitude']), float(inputs['latitude'])) else: plotoptions[p] = request.inputs[p][0].data files = glob.glob(os.path.join(rundir, 'outputs', '*_group*.txt')) if len(files) == 0: raise InvalidParameterValue( "Unable to find any output files. File names must be named '*_group*.txt'" ) if 'timestamp' in request.inputs: request.inputs['summarise'][0].data = 'NA' LOGGER.debug("Plot options: %s" % plotoptions) response.update_status("Processed plot parameters", 5) tot_plots = get_num_dates( start=datetime.strptime(inputs['startdate'], "%Y-%m-%d"), end=datetime.strptime(inputs['enddate'], "%Y-%m-%d"), sum=request.inputs['summarise'][0].data, type=inputs['timestamp']) # We need to find all the groups and loop through them one at a time! groups = {} for filename in os.listdir(os.path.join(rundir, 'outputs')): groupnum = filename[14] try: groupnum = int(groupnum) except: raise Exception("Cannot identify groupnumber %s" % groupnum) if groupnum in groups: shutil.copy(os.path.join(rundir, 'outputs', filename), groups[groupnum]) else: groups[groupnum] = tempfile.mkdtemp() shutil.copy(os.path.join(rundir, 'outputs', filename), groups[groupnum]) ngroups = len(groups) tot_plots = tot_plots * ngroups plots_made = 0 response.update_status("Plotting", 10) oldper = 10 for groupnum, tmpdir in sorted(groups.items()): if request.inputs['summarise'][0].data != 'NA': s = Sum(tmpdir) if request.inputs['summarise'][0].data == 'week': for week in range(1, 53): s.sumWeek(week) if len(s.files) == 0: LOGGER.debug("No files found for week %s" % week) continue plotoptions[ 'caption'] = "{} {} {} {}: {} week {} sum (UTC)".format( s.runname, s.averaging, s.altitude, s.direction, s.year, week) plotoptions['outfile'] = "{}_{}_{}_{}_weekly.png".format( s.runname, s.altitude.strip('()'), s.year, week) try: drawMap(s, 'total', **plotoptions) LOGGER.debug("Plotted %s" % plotoptions['outfile']) except: LOGGER.error("Failed to plot %s" % plotoptions['outfile']) plots_made += 1 newper = 10 + int((plots_made / float(tot_plots)) * 85) if oldper != newper: response.update_status("Plotting", newper) oldper = newper elif request.inputs['summarise'][0].data == 'month': for month in range(1, 13): s.sumMonth(str(month)) if len(s.files) == 0: LOGGER.debug("No files found for month %s" % month) continue plotoptions[ 'caption'] = "{} {} {} {}: {} {} sum (UTC)".format( s.runname, s.averaging, s.altitude, s.direction, s.year, calendar.month_name[month]) plotoptions['outfile'] = "{}_{}_{}_{}_monthly.png".format( s.runname, s.altitude.strip('()'), s.year, month) try: drawMap(s, 'total', **plotoptions) LOGGER.debug("Plotted %s" % plotoptions['outfile']) except: LOGGER.error("Failed to plot %s" % plotoptions['outfile']) plots_made += 1 newper = 10 + int((plots_made / float(tot_plots)) * 85) if oldper != newper: response.update_status("Plotting", newper) oldper = newper elif request.inputs['summarise'][0].data == 'all': s.sumAll() plotoptions['caption'] = "{} {} {} {}: Summed (UTC)".format( s.runname, s.averaging, s.altitude, s.direction) plotoptions['outfile'] = "{}_{}_summed_all.png".format( s.runname, s.altitude.strip('()')) try: drawMap(s, 'total', **plotoptions) LOGGER.debug("Plotted %s" % plotoptions['outfile']) except: LOGGER.error("Failed to plot %s" % plotoptions['outfile']) plots_made += 1 newper = 10 + int((plots_made / float(tot_plots)) * 85) if oldper != newper: response.update_status("Plotting", newper) oldper = newper else: for filename in os.listdir(tmpdir): if '_group' in filename and filename.endswith('.txt'): if request.inputs['summarise'][0].data == 'day': #s = Sum(tmpdir) date = util.shortname(filename) s.sumDay(date) plotoptions[ 'caption'] = "{} {} {} {}: {}{}{} day sum (UTC)".format( s.runname, s.averaging, s.altitude, s.direction, s.year, s.month, s.day) plotoptions[ 'outfile'] = "{}_{}_{}{}{}_daily.png".format( s.runname, s.altitude.strip('()'), s.year, s.month, s.day) try: drawMap(s, 'total', **plotoptions) LOGGER.debug("Plotted %s" % plotoptions['outfile']) except: LOGGER.error("Failed to plot %s" % plotoptions['outfile']) plots_made += 1 newper = 10 + int( (plots_made / float(tot_plots)) * 85) if oldper != newper: response.update_status("Plotting", newper) oldper = newper elif request.inputs['summarise'][0].data == 'NA': n = Name(os.path.join(tmpdir, filename)) if 'timestamp' in request.inputs: timestamp = datetime.strftime( request.inputs['timestamp'][0].data, "%d/%m/%Y %H:%M UTC") LOGGER.debug("Reformatted time: %s" % timestamp) if timestamp in n.timestamps: try: drawMap(n, timestamp, **plotoptions) LOGGER.debug("Plotted %s" % timestamp) except: LOGGER.error("Failed to plot %s" % timestamp) plots_made += 1 newper = 10 + int( (plots_made / float(tot_plots)) * 85) if oldper != newper: response.update_status( "Plotting", newper) oldper = newper break else: for column in n.timestamps: try: drawMap(n, column, **plotoptions) LOGGER.debug("Plotted %s" % column) except: LOGGER.error("Failed to plot %s" % column) plots_made += 1 newper = 10 + int( (plots_made / float(tot_plots)) * 85) if oldper != newper: response.update_status( "Plotting", newper) oldper = newper # Finished plotting so will now delete temp directory shutil.rmtree(tmpdir) # Outputting different response based on the number of plots generated response.update_status("Formatting output", 95) if not os.path.exists(plotoptions['outdir']): LOGGER.debug("Did not create any plots") response.outputs['FileContents'].data_format = FORMATS.TEXT response.outputs[ 'FileContents'].data = "No plots created, check input options" else: if len(os.listdir(plotoptions['outdir'])) == 1: LOGGER.debug("Only one output plot") response.outputs['FileContents'].data_format = Format( 'image/png') response.outputs['FileContents'].file = os.path.join( plotoptions['outdir'], os.listdir(plotoptions['outdir'])[0]) else: zippedfile = "{}_plots".format( request.inputs['filelocation'][0].data) shutil.make_archive(zippedfile, 'zip', plotoptions['outdir']) LOGGER.debug("Zipped file: %s (%s bytes)" % (zippedfile + '.zip', os.path.getsize(zippedfile + '.zip'))) response.outputs['FileContents'].data_format = FORMATS.SHP response.outputs['FileContents'].file = zippedfile + '.zip' response.update_status("done", 100) return response