├── .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")
--------------------------------------------------------------------------------