├── .gitignore ├── requirements.txt ├── {{cookiecutter.project_slug}} ├── launch_server.bat ├── static │ └── templates │ │ └── base.html ├── panelApps │ ├── datasources.py │ └── pn_app.py └── main.py ├── README.md ├── cookiecutter.json └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | holoviews 2 | fastapi 3 | uvicorn[standard] -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/launch_server.bat: -------------------------------------------------------------------------------- 1 | CALL "{{cookiecutter.conda_activate_path}}" {{cookiecutter.VENV_NAME}} 2 | 3 | python main.py 4 | 5 | PAUSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cookiecutter-fastapi-panel-python 2 | Cookiecutter template for FastAPI + Panel projects in Python 3 | 4 | Loads a very basic demo of a picklist widget with linked table and bar chart visuals 5 | 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/static/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{PROJECT_NAME}} 5 | 6 | 7 | {{script|safe}} 8 | 9 | -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | {"AUTHOR_NAME":"Default", 2 | "project_slug":"fastapi_panel_demo", 3 | "PROJECT_NAME":"FastAPI Panel Demo", 4 | "HOST_IP":"127.0.0.6", 5 | "PANEL_PORT":5000, 6 | "FASTAPI_PORT":8000, 7 | "SECRET_KEY":"CHANGE_THIS", 8 | "VENV_NAME":"venv_fastapi_panel", 9 | "conda_activate_path":"C:\\anaconda3\\Scripts\\activate.bat", 10 | "_copy_without_render": [ 11 | "*.html", 12 | "*.js", 13 | "*.map", 14 | "*.css" 15 | ]} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 {{cookiecutter.AUTHOR_NAME}} 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/panelApps/datasources.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | 3 | def demo_df(option): 4 | import random 5 | 6 | if option=='Option 1': 7 | 8 | n = 20 9 | s1_opts = ['Apple','Pear','Banana'] 10 | s2_opts = ['Farm','Store','Market'] 11 | 12 | series1=[] 13 | series2=[] 14 | 15 | for i in range(0,n): 16 | series1.append(random.choice(s1_opts)) 17 | series2.append(random.choice(s2_opts)) 18 | 19 | df = pd.DataFrame(zip(series1,series2),columns=['Fruit','Source']) 20 | 21 | return df 22 | 23 | elif option=='Option 2': 24 | 25 | n = 20 26 | s1_opts = ['Strawberries','Blueberries','Raspberries'] 27 | s2_opts = ['Farm','Store','Market'] 28 | 29 | series1=[] 30 | series2=[] 31 | 32 | for i in range(0,n): 33 | series1.append(random.choice(s1_opts)) 34 | series2.append(random.choice(s2_opts)) 35 | 36 | df = pd.DataFrame(zip(series1,series2),columns=['Fruit','Source']) 37 | 38 | return df 39 | 40 | else: 41 | raise(ValueError(f'unexpected choice {option}')) -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/panelApps/pn_app.py: -------------------------------------------------------------------------------- 1 | import panel as pn 2 | import holoviews as hv 3 | from holoviews.streams import Stream, param, Params 4 | from . import datasources 5 | 6 | hv.extension('bokeh') 7 | 8 | class createApp(): 9 | def __init__(self): 10 | ''' 11 | Widgets 12 | ''' 13 | #main plot picklists 14 | self.selection_picklist = pn.widgets.Select(options=['Option 1','Option 2',], 15 | name='Picklist Options', 16 | value='Option 1', 17 | height=20, 18 | width=250) 19 | 20 | @pn.depends(option=self.selection_picklist.param.value) 21 | def get_data(option): 22 | df = datasources.demo_df(option) 23 | return hv.Table(df) 24 | 25 | self.table = hv.DynamicMap(get_data) 26 | 27 | self.barchart = self.table.apply(self.gen_barchart) 28 | 29 | self.set_layout() 30 | 31 | def gen_barchart(self,table): 32 | summary_df = table.data.groupby(['Fruit']).agg({'Source':'count'}).reset_index() 33 | 34 | return hv.Bars(summary_df,kdims=['Fruit'],vdims=['Source']) 35 | 36 | 37 | def set_layout(self): 38 | self.gspec = pn.GridSpec(sizing_mode = "fixed") 39 | 40 | #top row 41 | self.gspec[0,:] = pn.Row(self.selection_picklist,sizing_mode='fixed') 42 | 43 | #main section 44 | self.gspec[1:3,:] = pn.Row(self.table,self.barchart,height=300,width=300) 45 | 46 | self.layout = self.gspec.servable() 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/main.py: -------------------------------------------------------------------------------- 1 | import panel as pn 2 | from fastapi import FastAPI, Request 3 | from fastapi.staticfiles import StaticFiles 4 | from fastapi.templating import Jinja2Templates 5 | from bokeh.embed import server_session, server_document 6 | from bokeh.util.token import generate_session_id 7 | import logging 8 | import uvicorn 9 | 10 | from panelApps.pn_app import createApp 11 | 12 | app = FastAPI() 13 | 14 | #setup for static resources 15 | app.mount("/static",StaticFiles(directory='static'),name='static') 16 | templates = Jinja2Templates(directory="static/templates") 17 | 18 | SECRET_KEY = '{{cookiecutter.SECRET_KEY}}' #in a real system; swap this with a secret key outside of your source code 19 | HOST_IP = '{{cookiecutter.HOST_IP}}' 20 | PANEL_PORT = {{cookiecutter.PANEL_PORT}} 21 | FASTAPI_PORT = {{cookiecutter.FASTAPI_PORT}} 22 | PROJECT_NAME = '{{cookiecutter.PROJECT_NAME}}' 23 | 24 | logger = logging.getLogger('uvicorn.error') 25 | 26 | @app.on_event("startup") 27 | async def startup_event(): 28 | ''' 29 | add handler to the base uvicorn logger 30 | ''' 31 | handler = logging.StreamHandler() 32 | handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")) 33 | logger.addHandler(handler) 34 | logger.info("Started Logger") 35 | 36 | 37 | @app.get("/") 38 | async def bokeh_app_page(request: Request): 39 | ''' 40 | Landing page template 41 | ''' 42 | url = f'http://{HOST_IP}:{PANEL_PORT}/app' 43 | script = server_session( 44 | session_id=generate_session_id(SECRET_KEY, signed=True), url=url 45 | ) 46 | 47 | return templates.TemplateResponse("base.html", {"request": request,"script":script,"PROJECT_NAME":PROJECT_NAME}) 48 | 49 | #launch bokeh server 50 | panel_app = pn.serve({'/app': lambda :createApp().layout}, 51 | port=PANEL_PORT, 52 | address=f"{HOST_IP}", 53 | allow_websocket_origin=[f"{HOST_IP}:{PANEL_PORT}"], 54 | show=False, 55 | secret_key = SECRET_KEY, 56 | sign_sessions = True, 57 | generate_session_ids=True, 58 | num_process = 3) 59 | 60 | if __name__ == "__main__": 61 | uvicorn.run("main:app", host=HOST_IP, port=FASTAPI_PORT, log_level="info") --------------------------------------------------------------------------------