Last active
June 22, 2022 17:29
-
-
Save charbonnierg/f62167f1b0dc39e517afbcf809e85d8e to your computer and use it in GitHub Desktop.
Server static files along a FastAPI application using starlette
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
from pathlib import Path | |
from typing import Callable, Optional | |
from fastapi import FastAPI | |
from starlette.applications import Starlette | |
from starlette.routing import Mount | |
from starlette.staticfiles import StaticFiles | |
from starlette.types import Scope | |
if os.environ.get("DEV"): | |
# I work with the following layout during development: | |
# \__ build | |
# \__ index.html | |
# \__ backend | |
# \__ quara | |
# \__agent | |
# \__ server.py | |
# So: | |
# parent => ~/backend/quara/agent | |
# parent.parent => ~/backend/quara | |
# parent.parent.parent => ~/backend | |
# parent.parent.parent.parent / "build" => ~/build | |
STATIC_ROOT = os.environ.get( | |
"STATIC_ROOT", Path(__file__).parent.parent.parent.parent / "build" | |
) | |
else: | |
# In production we expect HTML build directory to be found in ~/backend/quara/agent/html | |
# But it's always possible to use STATIC_ROOT environment variable to configure location of HTML build directory. | |
STATIC_ROOT = os.environ.get("STATIC_ROOT", Path(__file__).parent / "html") | |
class SPAStaticFiles(StaticFiles): | |
"""Serve a single page application""" | |
async def get_response(self, path: str, scope: Scope): | |
response = await super().get_response(path, scope) | |
if response.status_code == 404: | |
response = await super().get_response(".", scope) | |
return response | |
def compose( | |
app: Starlette, | |
api: FastAPI, | |
web_directory: str, | |
) -> Starlette: | |
"""Attach the website to the Starlette application""" | |
# First create an instance of StaticFiles to serve the HTML directory | |
website = SPAStaticFiles(directory=web_directory, html=True) | |
# Add routes to the app provided as argument | |
app.router.routes.extend( | |
[ | |
Mount("/api", api, name="api"), | |
Mount("/", website, name="web"), | |
] | |
) | |
# Return the application | |
return app | |
def create_server( | |
app_factory: Callable[..., FastAPI], html_root: Optional[str] = None | |
) -> Starlette: | |
# Create a Starlette instance | |
server = Starlette() | |
# Create the FastAPI instance | |
api = app_factory() | |
# Check if web directory was provided | |
if html_root is None: | |
html_root = STATIC_ROOT / "website" | |
# Compose the FastAPI application and the HTML file server into the server | |
compose(server, api, html_root) | |
# Return the Starlette instance | |
return server | |
def factory() -> None: | |
"""A factory used by uvicorn to start the application. | |
Uvicorn factories are called without argument. | |
They should return an ASGI application such as a Starlette application or FastAPI application. | |
In our case, we return a Starlette application which is has two routes: | |
- A mounted FastAPI application on "/api": The API | |
- A mounted StaticFile application on "/": The frontend | |
To start a new server simply run: | |
$ uvicorn --factory quara.agent.server | |
Configuration should be provided either through environment variables or through files. | |
""" | |
# Let's say we've got a function which returns a FastAPI application in a module | |
# No argument A FastAPI instance | |
# | | | |
# V V | |
# create_app: () -> FastAPI | |
from quara.agent.app import create_app | |
# Use create_server to return a server as Starlette instance | |
return create_server(create_app) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment