Skip to content

Instantly share code, notes, and snippets.

@charbonnierg
Last active June 22, 2022 17:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save charbonnierg/f62167f1b0dc39e517afbcf809e85d8e to your computer and use it in GitHub Desktop.
Save charbonnierg/f62167f1b0dc39e517afbcf809e85d8e to your computer and use it in GitHub Desktop.
Server static files along a FastAPI application using starlette
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