import matplotlib matplotlib.use('Agg') import os import os.path import json import jinja2 import matplotlib.pyplot as plt import pandas as pd import numpy as np import copy import cherrypy from cherrypy.lib.static import serve_file from cherrypy.lib.static import serve_fileobj try: import StringIO as io # python2 except Exception: import io # python3 try: from . import model except Exception: import model try: from . import View except Exception: try: import View except Exception: from . import view as View # Settings include_df_index = False ROOT_DIR = os.path.dirname(os.path.realpath(__file__)) templateLoader = jinja2.FileSystemLoader(searchpath=ROOT_DIR) templateEnv = jinja2.Environment(loader=templateLoader) class Root(object): def __init__( self, templateVars=None, title="", inputs=[], outputs=[], controls=[], tabs=None, spinnerFile=None, getJsonDataFunction=None, getDataFunction=None, getTableFunction=None, getPlotFunction=None, getImageFunction=None, getD3Function=None, getCustomCSSFunction=None, getCustomJSFunction=None, getCustomHeadFunction=None, getHTMLFunction=None, getDownloadFunction=None, noOutputFunction=None, storeUploadFunction=None, prefix='/' ): # populate template dictionary for creating input,controler, and output HTML and javascript if templateVars is not None: self.templateVars = templateVars else: self.templateVars = {} self.templateVars['title'] = title if prefix[-1] == '/': self.templateVars['prefix'] = prefix[:-1] else: self.templateVars['prefix'] = prefix # necessary to ensure that spyre apps prior to version 0.2.0 still work for input in inputs: if 'input_type' in input: input['type'] = input['input_type'] if 'variable_name' in input: input['key'] = input['variable_name'] if 'linked_variable_name' in input: input['linked_key'] = input['linked_variable_name'] if 'linked_variable_type' in input: input['linked_type'] = input['linked_variable_type'] self.templateVars['inputs'] = inputs for control in controls: if 'control_type' in control: control['type'] = control['control_type'] if 'control_id' in control: control['id'] = control['control_id'] self.templateVars['controls'] = controls for output in outputs: if 'output_type' in output: output['type'] = output['output_type'] if 'output_id' in output: output['id'] = output['output_id'] self.templateVars['outputs'] = outputs if tabs is not None: self.templateVars['tabs'] = tabs if spinnerFile is not None: self.templateVars['spinnerFile'] = spinnerFile self.defaultTemplateVars = self.templateVars self.getJsonData = getJsonDataFunction self.getData = getDataFunction self.getTable = getTableFunction self.getPlot = getPlotFunction self.getImage = getImageFunction self.getD3 = getD3Function self.getCustomJS = getCustomJSFunction self.getCustomCSS = getCustomCSSFunction self.getCustomHead = getCustomHeadFunction self.getHTML = getHTMLFunction self.noOutput = noOutputFunction self.getDownload = getDownloadFunction self.storeUpload = storeUploadFunction d3 = self.getD3() custom_js = self.getCustomJS() custom_css = self.getCustomCSS() custom_head = self.getCustomHead() self.templateVars['d3js'] = d3['js'] self.templateVars['d3css'] = d3['css'] self.templateVars['custom_js'] = custom_js self.templateVars['custom_css'] = custom_css self.templateVars['custom_head'] = custom_head v = View.View() self.templateVars['document_ready_js'] = "" self.templateVars['js'] = v.getJS() self.templateVars['css'] = v.getCSS() self.upload_file = None @cherrypy.expose def index(self, **args): # create a deepcopy so other people's changes aren't cached self.templateVars = copy.deepcopy(self.defaultTemplateVars) clean_args = self.clean_args(args) self.use_custom_input_values(clean_args) v = View.View() template = jinja2.Template(v.getHTML()) return template.render(self.templateVars) def use_custom_input_values(self, args): input_registration = {} index = 0 for input in self.templateVars['inputs']: input_key = input['key'] # register inputs to be so we can look them up by their variable name later if 'action_id' in input: input_registration[input_key] = { "type": input['type'], "action_id": input['action_id'] } else: input_registration[input_key] = { "type": input['type'], "action_id": None } if input_key in args.keys(): # use value from request input_value = args[input_key] elif 'value' in input: # use value from template input_value = input['value'] else: # no value specified index += 1 continue # use the params passed in with the url switch out the default input values if input['type'] in ['text', 'slider', 'searchbox']: self.templateVars['inputs'][index]['value'] = input_value if input['type'] in ['radiobuttons', 'dropdown']: for option in input['options']: option['checked'] = (option['value'] == input_value) if input['type'] in ['checkboxgroup', 'multiple']: index2 = 0 for option in input['options']: if option['value'] in input_value: self.templateVars['inputs'][index]['options'][index2]['checked'] = True else: self.templateVars['inputs'][index]['options'][index2]['checked'] = False index2 += 1 index += 1 @cherrypy.expose def plot(self, **args): args = self.clean_args(args) p = self.getPlot(args) if p is None: return None d = model.Plot() buffer = d.getPlotPath(p) cherrypy.response.headers['Content-Type'] = 'image/png' return buffer.getvalue() @cherrypy.expose def image(self, **args): args = self.clean_args(args) img = self.getImage(args) if img is None: return None d = model.Image() buffer = d.getImagePath(img) cherrypy.response.headers['Content-Type'] = 'image/jpg' return buffer.getvalue() @cherrypy.expose def data(self, **args): args = self.clean_args(args) data = self.getJsonData(args) if data is None: return None cherrypy.response.headers['Content-Type'] = 'application/json' return json.dumps({'data': data, 'args': args}).encode('utf8') @cherrypy.expose def table(self, **args): args = self.clean_args(args) df = self.getTable(args) if df is None: return "" html = df.to_html(index=include_df_index, escape=False) i = 0 for col in df.columns: html = html.replace( '<th>{}'.format(col), '<th><a onclick="sortTable({},"table0");"><b>{}</b></a>'.format(i, col) ) i += 1 html = html.replace( 'border="1" class="dataframe"', 'class="sortable" id="sortable"' ) html = html.replace('style="text-align: right;"', '') cherrypy.response.headers['Content-Type'] = 'text/html' return html @cherrypy.expose def html(self, **args): args = self.clean_args(args) html = self.getHTML(args) if html is None: return "" cherrypy.response.headers['Content-Type'] = 'text/html' return html @cherrypy.expose def download(self, **args): args = self.clean_args(args) filepath = self.getDownload(args) if type(filepath).__name__ == "str": return serve_file(filepath, "application/x-download", "attachment", name='data.csv') if type(filepath).__name__ == "instance": file_obj = serve_fileobj( filepath.getvalue(), "application/x-download", "attachment", name='data.csv' ) return file_obj if type(filepath).__name__ == "StringIO": file_obj = serve_fileobj( filepath.getvalue().encode('utf-8'), "application/x-download", "attachment", name='data.csv' ) return file_obj else: return "error downloading file. filepath must be string of buffer" @cherrypy.expose def upload(self, xfile): self.storeUpload(xfile.file) @cherrypy.expose def no_output(self, **args): args = self.clean_args(args) self.noOutput(args) return '' @cherrypy.expose def spinning_wheel(self, **args): v = View.View() spinnerFile = self.templateVars.get('spinnerFile') buffer = v.getSpinningWheel(spinnerFile) cherrypy.response.headers['Content-Type'] = 'image/gif' return buffer.getvalue() def clean_args(self, args): for k, v in args.items(): # turn checkbox group string into a list if v.rfind("__list__") == 0: tmp = v.split(',') if len(tmp) > 1: args[k] = tmp[1:] else: args[k] = [] # convert to a number if v.rfind("__float__") == 0: args[k] = float(v[9:]) return args class App(object): title = "" # Will be used when there are more than one app in a site app_bar_html = None outputs = [] inputs = [] controls = [] tabs = None spinnerFile = None templateVars = None prefix = '/' def getJsonData(self, params): """turns the DataFrame returned by getData into a dictionary arguments: the params passed used for table or d3 outputs are forwarded on to getData """ try: return eval("self." + str(params['output_id']) + "(params)") except AttributeError: df = self.getData(params) if df is None: return None return df.to_dict(orient='records') def getData(self, params): """Override this function arguments: params (dict) returns: DataFrame """ return eval("self." + str(params['output_id']) + "(params)") def getTable(self, params): """Used to create html table. Uses dataframe returned by getData by default override to return a different dataframe. arguments: params (dict) returns: html table """ df = self.getData(params) if df is None: return None return df def getDownload(self, params): """Override this function arguments: params (dict) returns: path to file or buffer to be downloaded (string or buffer) """ df = self.getData(params) buffer = io.StringIO() df.to_csv(buffer, index=False, encoding='utf-8') filepath = buffer return filepath def storeUpload(self, file): """Override this function arguments: params (dict) returns: path to file or buffer to be downloaded (string or buffer) """ pass def getPlot(self, params): """Override this function arguments: params (dict) returns: matplotlib.pyplot figure """ try: return eval("self." + str(params['output_id']) + "(params)") except AttributeError: df = self.getData(params) if df is None: return None return df.plot() def getImage(self, params): """Override this function arguments: params (dict) returns: matplotlib.image (figure) """ return eval("self." + str(params['output_id']) + "(params)") def getHTML(self, params): """Override this function arguments: params (dict) returns: html (string) """ return eval("self." + str(params['output_id']) + "(params)") def noOutput(self, params): """Override this function A method for doing stuff that doesn't reququire an output (refreshing data, updating variables, etc.) arguments: params (dict) """ return eval("self." + str(params['output_id']) + "(params)") def getD3(self): d3 = {} d3['css'] = "" d3['js'] = "" return d3 def getCustomJS(self): """Override this function returns: string of javascript to insert on page load """ return "" def getCustomCSS(self): """Override this function returns: string of css to insert on page load """ return "" def getCustomHead(self): """Override this function returns: html to put in html header """ return "" def launch(self, host="local", port=8080, prefix='/', config=None): self.prefix = prefix webapp = self.getRoot() if host != "local": cherrypy.server.socket_host = '0.0.0.0' cherrypy.server.socket_port = port cherrypy.tree.mount(webapp, prefix) cherrypy.quickstart(webapp, config=config) def launch_in_notebook(self, port=9095, width=900, height=600): """launch the app within an iframe in ipython notebook""" from IPython.lib import backgroundjobs as bg from IPython.display import HTML jobs = bg.BackgroundJobManager() jobs.new(self.launch, kw=dict(port=port)) frame = HTML( '<iframe src=http://localhost:{} width={} height={}></iframe>' .format(port, width, height) ) return frame def getRoot(self): webapp = Root( templateVars=self.templateVars, title=self.title, inputs=self.inputs, outputs=self.outputs, controls=self.controls, tabs=self.tabs, spinnerFile=self.spinnerFile, getJsonDataFunction=self.getJsonData, getDataFunction=self.getData, getTableFunction=self.getTable, getPlotFunction=self.getPlot, getImageFunction=self.getImage, getD3Function=self.getD3, getCustomJSFunction=self.getCustomJS, getCustomCSSFunction=self.getCustomCSS, getCustomHeadFunction=self.getCustomHead, getHTMLFunction=self.getHTML, getDownloadFunction=self.getDownload, noOutputFunction=self.noOutput, storeUploadFunction=self.storeUpload, prefix=self.prefix ) return webapp class Site(object): """Creates a 'tree' of cherrypy 'Root' objects that allow for the creation of multiple apps with routes to different 'apps.' Calling the launch method will return """ def __init__(self, appobj): self.site_app_bar = list() self.addIndex(appobj) def addIndex(self, appobj): self.site_app_bar.append(( "/", appobj.app_bar_html or appobj.title or "/" )) self.root = appobj().getRoot() def get_route(self, fullRoute): routeSplit = fullRoute.split('/') routeSplit.remove('') parent = self.root for route in routeSplit[:-1]: parent = getattr(parent, route) return parent, routeSplit[-1] def addApp(self, appobj, fullRoute): parent, route = self.get_route(fullRoute) self.site_app_bar.append(( fullRoute, appobj.app_bar_html or appobj.title or route )) setattr(parent, route, appobj().getRoot()) def getRoot(self): """A convenience method to make the site API similar to the app API, in terms of how the cherrypy Root object is retrieved""" return self.root def launch(self, host="local", port=8080): """Calling the Launch method on a Site object will serve the top node of the cherrypy Root object tree""" # Need to add in the appbar if many apps self.root.templateVars['app_bar'] = self.site_app_bar for fullRoute, _ in self.site_app_bar[1:]: parent, route = self.get_route(fullRoute) parent.__dict__[route].templateVars['app_bar'] = self.site_app_bar if host != "local": cherrypy.server.socket_host = '0.0.0.0' cherrypy.server.socket_port = port cherrypy.quickstart(self.root) class Launch(App): """Warning: This class is depricated. Use App instead""" if __name__ == '__main__': app = App() app.launch()