Fastapi getting static routes right? - python

My setup is such that i want to run Svelte and FastAPI on the same Heroku instance.
I managed to get the static routes in this way that I have two apps:
app = FastAPI(title="Main App")
api_app=FastAPI(title="Api App")
app.mount('/api', api_app)
app.mount('/build', StaticFiles(directory="frontend-svelte/public/build", html=True), name="build")
app.mount('/', StaticFiles(directory="frontend-svelte/public", html=True), name="static")
Which is great for serving index.html - however, svelte would like to have the internal routes in a way that anything that isn't /api or /build serves index.html.
This is why I added
#app.route("/{full_path:path}")
async def catch_all(request: Request, full_path: str):
print("full_path: "+full_path)
return templates.TemplateResponse("index.html", {"request": request})
However it returns a 404 and I don't know whats wrong?

First, you need to define your route before mounts.
Second, #app.route don't work with {full_path:path}. If you need only to serve static files, you can use get. Or copy your functoin for post, delete and put methods.
app = FastAPI(title="Main App")
api_app = FastAPI(title="Api App")
#app.get("/{full_path:path}")
async def catch_all(full_path: str, request: Request):
print("full_path: "+full_path)
return templates.TemplateResponse("index.html", {"request": request})
app.mount("/api", api_app)
app.mount(
"/build",
StaticFiles(directory="frontend-svelte/public/build", html=True),
name="build",
)
app.mount(
"/", StaticFiles(directory="frontend-svelte/public", html=True), name="static"
)
If you only need to serve static files for other app, nginx will work better.

Related

How to reuse static files across multiple APIRouters?

If I've got the following static file: static/example.png and I mount my staticfiles like so:
app.mount('/static', StaticFiles(directory='static'), name='static')
I can now use that file in my HTML as static/example.png, so long as I only use views that are registered directly to my main app, for example:
#app.get('/home', response_type=HTMLResponse)
def home(request: Request):
return '<img src="static/example.png" ...>'
And this works fine. However, if I create a new APIRouter, like so:
router = APIRouter(prefix='/dashboards')
app.include_router(router=router)
Now if I try to use static/example.png in a view that's registered to the new 'dashboards' router, for example like:
#router.get('/home', response_type=HTMLResponse)
def dashboards_home(request: Request):
return '<img src="static/example.png" ...>'
I get the following error:
INFO: 127.0.0.1:52771 - "GET /dashboards/static/example.png HTTP/1.1" 404 Not Found
Basically, it seems that the APIRouter prefix gets prepended to every url inside my HTML, such as for href or src tag attributes. So instead of being treated it as a local static file, it gets treated as a web URL.
This means that I can't reuse my static files across APIRouters without mounting a new StaticFiles instance against each one, making a copy of the static file under a new folder named after the router prefix, and then adding the corresponding router prefix inside my HTML (in the above example, I'd have to reference the file as dashboards/static/example.png and make the corresponding file copy).
This then means that I need a copy of each static file for every APIRouter that needs access to it. Even worse, I can't use jinja2 templates with inheritance features, because depending on where the parent template is being extended from, the static file it references would need different prefixes to be applied to it.
Is there any way I can get all views belonging to an APIRouter to NOT apply their router's prefix to any urls used inside the HTML they serve up?
UPDATE
You can reference the absolute path by prefixing static in your html with a /
Below is a fully working example of this:
from fastapi import APIRouter, FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
app = FastAPI()
app.mount('/static', StaticFiles(directory='static'), name='static')
#app.get('/home', response_class=HTMLResponse)
def home(request: Request):
return "<link rel='stylesheet' href='/static/test.css'><h1>hello from home</h1>"
# ---------------------------------------^ absolute path!
router = APIRouter(prefix='/myrouter')
#router.get('/route', response_class=HTMLResponse)
def route(request: Request):
return "<link rel='stylesheet' href='/static/test.css'><h1>hello from home</h1>"
# ---------------------------------------^ absolute path!
app.include_router(router)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
OLD ANSWER
You can use relative pointers. The below is a full example, it should run as-is:
from fastapi import APIRouter, FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
app = FastAPI()
app.mount('/static', StaticFiles(directory='static'), name='static')
#app.get('/home', response_class=HTMLResponse)
def home(request: Request):
return "<link rel='stylesheet' href='static/test.css'><h1>hello from home</h1>"
router = APIRouter(prefix='/myrouter')
#router.get('/route', response_class=HTMLResponse)
def route(request: Request):
return "<link rel='stylesheet' href='../static/test.css'><h1>hello from home</h1>"
# ---------------------------------------^ relative path!
app.include_router(router)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
This way, you can just have one StaticFiles declaration.

How to capture arbitrary paths at one route in FastAPI?

I'm serving React app from FastAPI by
mounting
app.mount("/static", StaticFiles(directory="static"), name="static")
#app.route('/session')
async def renderReactApp(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
by this React app get served and React routing also works fine at client side
but as soon as client reloads on a route which is not defined on server but used in React app FastAPI return not found to fix this I did something as below.
#app.route('/network')
#app.route('/gat')
#app.route('/session')
async def renderReactApp(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
but it seems weird and wrong to me as I need to add every route at the back-end as well as at frontend.
I'm sure there must be something like Flask #flask_app.add_url_rule('/<path:path>', 'index', index) in FastAPI which will server all arbitrary path
Since FastAPI is based on Starlette, you can use what they call "converters" with your route parameters, using type path in this case, which "returns the rest of the path, including any additional / characers."
See https://www.starlette.io/routing/#path-parameters for reference.
If your react (or vue or ...) app is using a base path, you can do something like this, which assigns anything after /my-app/ to the rest_of_path variable:
#app.get("/my-app/{rest_of_path:path}")
async def serve_my_app(request: Request, rest_of_path: str):
print("rest_of_path: "+rest_of_path)
return templates.TemplateResponse("index.html", {"request": request})
If you are not using a unique base path like /my-app/ (which seems to be your use case), you can still accomplish this with a catch-all route, which should go after any other routes so that it doesn't overwrite them:
#app.route("/{full_path:path}")
async def catch_all(request: Request, full_path: str):
print("full_path: "+full_path)
return templates.TemplateResponse("index.html", {"request": request})
(In fact you would want to use this catch-all regardless in order to catch the difference between requests for /my-app/ and /my-app)
As #mecampbellsoup pointed out: there are usually other static files that need to be served with an application like this.
Hopefully this comes in handy to someone else:
import os
from typing import Tuple
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
app = FastAPI()
class SinglePageApplication(StaticFiles):
"""Acts similar to the bripkens/connect-history-api-fallback
NPM package."""
def __init__(self, directory: os.PathLike, index='index.html') -> None:
self.index = index
# set html=True to resolve the index even when no
# the base path is passed in
super().__init__(directory=directory, packages=None, html=True, check_dir=True)
async def lookup_path(self, path: str) -> Tuple[str, os.stat_result]:
"""Returns the index file when no match is found.
Args:
path (str): Resource path.
Returns:
[tuple[str, os.stat_result]]: Always retuens a full path and stat result.
"""
full_path, stat_result = await super().lookup_path(path)
# if a file cannot be found
if stat_result is None:
return await super().lookup_path(self.index)
return (full_path, stat_result)
app.mount(
path='/',
app=SinglePageApplication(directory='path/to/dist'),
name='SPA'
)
These modifications make the StaticFiles mount act similar to the connect-history-api-fallback NPM package.
Simple and effective solution compatible with react-router
I made a very simple function that it is fully compatible react-router and create-react-app applications (most use cases)
The function
from pathlib import Path
from typing import Union
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
def serve_react_app(app: FastAPI, build_dir: Union[Path, str]) -> FastAPI:
"""Serves a React application in the root directory `/`
Args:
app: FastAPI application instance
build_dir: React build directory (generated by `yarn build` or
`npm run build`)
Returns:
FastAPI: instance with the react application added
"""
if isinstance(build_dir, str):
build_dir = Path(build_dir)
app.mount(
"/static/",
StaticFiles(directory=build_dir / "static"),
name="React App static files",
)
templates = Jinja2Templates(directory=build_dir.as_posix())
#app.get("/{full_path:path}")
async def serve_react_app(request: Request, full_path: str):
"""Serve the react app
`full_path` variable is necessary to serve each possible endpoint with
`index.html` file in order to be compatible with `react-router-dom
"""
return templates.TemplateResponse("index.html", {"request": request})
return app
Usage
import uvicorn
from fastapi import FastAPI
app = FastAPI()
path_to_react_app_build_dir = "./frontend/build"
app = serve_react_app(app, path_to_react_app_build_dir)
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8001)
Let's say you have a app structure like this:
├── main.py
└── routers
└── my_router.py
And the routers we created in my_router.py
from fastapi import APIRouter
router = APIRouter()
#router.get("/some")
async def some_path():
pass
#router.get("/path")
async def some_other_path():
pass
#router.post("/some_post_path")
async def some_post_path():
pass
Let's dive in to the main.py first we need to import our router we declared with
from routers import my_router
Then let's create a app instance
from fastapi import FastAPI
from routers import my_router
app = FastAPI()
So how do we add our routers?
from fastapi import FastAPI
from routers import my_router
app = FastAPI()
app.include_router(my_router.router)
You can also add prefix, tag, etc.
from fastapi import FastAPI
from routers import my_router
app = FastAPI()
app.include_router(
my_router.router,
prefix="/custom_path",
tags=["We are from router!"],
)
Let's check the docs
Here is an example of serving multiple routes (or lazy loading functions) using a single post url. The body of a request to the url would contain the name of a function to call and data to pass to the function if any. The *.py files in the routes/ directory contain the functions, and functions share the same name as their files.
project structure
app.py
routes/
|__helloworld.py
|_*.py
routes/helloworld.py
def helloworld(data):
return data
app.py
from os.path import split, realpath
from importlib.machinery import SourceFileLoader as sfl
import uvicorn
from typing import Any
from fastapi import FastAPI
from pydantic import BaseModel
# set app's root directory
API_DIR = split(realpath(__file__))[0]
class RequestPayload(BaseModel):
"""payload for post requests"""
# function in `/routes` to call
route: str = 'function_to_call'
# data to pass to the function
data: Any = None
app = FastAPI()
#app.post('/api')
async def api(payload: RequestPayload):
"""post request to call function"""
# load `.py` file from `/routes`
route = sfl(payload.route,
f'{API_DIR}/routes/{payload.route}.py').load_module()
# load function from `.py` file
func = getattr(route, payload.route)
# check if function requires data
if ('data' not in payload.dict().keys()):
return func()
return func(payload.data)
This example returns {"hello": "world"} with the post request below.
curl -X POST "http://localhost:70/api" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"route\":\"helloworld\",\"data\":{\"hello\": \"world\"}}"
The benefit of this setup is that a single post url can be used to complete any type of request (get, delete, put, etc), as the "type of request" is the logic defined in the function. For example, if get_network.py and delete_network.py are added to the routes/ directory
routes/get_network.py
def get_network(id: str):
network_name = ''
# logic to retrieve network by id from db
return network_name
routes/delete_network.py
def delete_network(id: str):
network_deleted = False
# logic to delete network by id from db
return network_deleted
then a request payload of {"route": "get_network", "data": "network_id"} returns a network name, and {"route": "delete_network", "data": "network_id"} would return a boolean indicating wether the network was deleted or not.

Routing issue on Single Page Application on Elastic Beanstalk

Using Flask as the web server, and Angular as my SPA in a /static directory. I implemented a catch-all end point on my Flask app to try solve an issue where users were being given a 404 when they refresh a page, but it did not solve the issue. Does anyone know how to fix this?
The catch-all looks like:
#application.route('/', defaults={'path': ''})
#application.route('/static/<path:path>')
def main(path):
return render_template('index.html')
I also implemented a redirect if a 404 is thrown, but this hasn't helped either:
#application.errorhandler(Exception)
#cross_origin()
def main_404(*args, **kwargs):
logger.info("Routing passed to web application...")
return render_template('index.html')
Try using error handlers if you want to catch errors:
#application.errorhandler(404)
def page_not_found(error):
app.logger.error('Page not found: %s', (request.path))
return render_template('404.html'), 404

Injecting a Flask Request into another Flask App

Is there a way to inject a Flask request object into a different Flask app. This is what I'm trying to do:
app = flask.Flask(__name__)
#app.route('/foo/<id>')
def do_something(id):
return _process_request(id)
def say_hello(request):
# request is an instance of flask.Request.
# I want to inject it into 'app'
I'm trying this with Google Cloud Functions, where say_hello() is a function that is invoked by the cloud runtime. It receives a flask.Request as the argument, which I want to then process through my own set of routes.
I tried the following, which doesn't work:
def say_hello(request):
with app.request_context(request.environ):
return app.full_dispatch_request()
This responds with 404 errors for all requests.
Edit:
The simple way to implement say_hello() is as follows:
def say_hello(request):
if request.method == 'GET' and request.path.startswith('/foo/'):
return do_something(_get_id(request.path))
flask.abort(404)
This essentially requires me to write the route matching logic myself. I'm wondering if there's a way to avoid doing that, and instead use Flask's built-in decorators and routing capabilities.
Edit 2:
Interestingly, dispatching across apps work locally:
app = flask.Flask(__name__)
# Add app.routes here
functions = flask.Flask('functions')
#functions.route('/', defaults={'path': ''})
#functions.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE'])
def catch_all(path):
with app.request_context(flask.request.environ):
return app.full_dispatch_request()
if __name__ == '__main__':
functions.run()
But the same technique doesn't seem to work on GCF.
I wouldn't recommend this method, but this is technically possible by abusing the request stack and rewriting the current request and re-dispatching it.
However, you'll still need to do some type of custom "routing" to properly set the url_rule, as the incoming request from GCF won't have it (unless you explicitly provide it via the request):
from flask import Flask, _request_ctx_stack
from werkzeug.routing import Rule
app = Flask(__name__)
#app.route('/hi')
def hi(*args, **kwargs):
return 'Hi!'
def say_hello(request):
ctx = _request_ctx_stack.top
request = ctx.request
request.url_rule = Rule('/hi', endpoint='hi')
ctx.request = request
_request_ctx_stack.push(ctx)
return app.dispatch_request()

Python : Flask : Catch All Url for login_required

I want the user to not be able to fetch any assets if they aren't logged in. Can any one tell me why the below doesn't work for :
http://domain-name:5000/static/index.html.
The user gets served the index.html file even though they are not logged in.
lm.login_view = "/static/login.html"
#app.route('/',defaults={'path':''})
#app.route('/static/<path:path>')
#login_required
def root():
logging.debug('Login Required - Authenticated user. Will Redirect')
return redirect(path)
Thanks!
By default if exist static folder flask have static endpoint which maped static url path to static folder path. You can change static_url_path or static_folder flask argument to another (not static).
If you want require login for static endpoint then you can try next code:
#app.before_request
def check_login():
if request.endpoint == 'static' and not current_user.is_authenticated():
abort(401)
return None
or override send_static_file view function:
def send_static_file(self, filename):
if not current_user.is_authenticated():
abort(401)
return super(Flask, self).send_static_file(filename)

Categories