How to reuse static files across multiple APIRouters? - python

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.

Related

Fastapi getting static routes right?

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.

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.

flask inner blueprint cannot find templates

I'm creating a website for my CV using flask for front-end and back-end.
Each page contains a layout of an header, footer & profile info + container of the current selected content.
In order to support this structure I've created a package called contact that contains several blueprints (blog_blueprint, homepage_blueprint, etc...) and register them to the application on import
my project structure is:
Clerification - there are no templates with the same name under different blueprints
first of all, Is that considered a good practice? if not - what are the recommandations for case like this?
second, while running the code and enter it I'm getting an error indecating that it cannot find somepage.html in my homepage_blueprint...
homepage blueprint code:
from project import db
from project.models import About, Service, Skill
from flask import render_template
from flask.blueprints import Blueprint
homepage_blueprint = Blueprint('Home Page', __name__)
#homepage_blueprint.route('/home/')
def homepage():
return render_template(
'homepage.html',
about=_get_about_me(),
services=_get_my_services(),
desing_skills=_get_design_skills(),
code_skills=_get_code_skills())
####################
# Helper functions #
####################
def _get_about_me():
return About.query.one()
def _get_my_services():
return Service.query.all()
def _get_design_skills():
return _get_skill('Design')
def _get_code_skills():
return _get_skill('Code')
def _get_skill(type: str):
return db.session.query(Skill).filter_by(type=type).all()

How to use url_map.iter_rules with blueprint object instead of app

I have a blueprint object "api" and a apis.py file where I have many APIs defined with api.route annotation. eg:
#api.route('/', methods=['GET'])
def get_info():
I want to iterate and get summary of all the APIs I have same as what we get when we use "url_map.iter_rules" on app object. How can we do this using api blueprint object? I have registered the blueprint in my init.py file using
from .api_1 import api as api_blueprint
app.register_blueprint(api_blueprint)
I think if you call app.url_map.iter_rules() after you've registered the blueprint you'll get all of the endpoints of the subdomains too, e.g.
api.py
from flask import Blueprint
api = Blueprint('api', __name__)
#api.route('/')
def call_api():
return ""
init.py:
from flask import Flask, Blueprint
from api import api
public = Blueprint('public', __name__)
#public.route('/')
def home():
return render_template('public/home.html')
app = Flask(__name__)
app.register_blueprint(public)
app.register_blueprint(api, subdomain='api')
print(list(app.url_map.iter_rules()))
[<Rule 'api|/' (GET, HEAD, OPTIONS) -> api.call_api>,
<Rule '/' (GET, HEAD, OPTIONS) -> public.home>,
<Rule '/static/<filename>' (GET, HEAD, OPTIONS) -> static>]
In case you find it useful, I made a function that shows me (only for testing) each of the url's according to the blueprints that are registered in the main application.
This is the only solution I have found to be able to print the endpoints separating them by the blueprint they belong to.
Of course you can create a function to be able to print the url_map of only one of the blueprints by passing the name in string format, or the blueprint itself.
Here are the examples:
from flask.logging import create_logger
def log_routes(app: Flask):
log = create_logger(app)
with app.app_context():
"""
Maps every single endpoint for blueprint registered in the main application.
Also shows methos available for each endpoint
"""
log.info('MAP ROUTER')
bps = app.blueprints
for bp in bps:
print('', end='\n')
log.info(f'BLUEPRINT RULES: "{bp}"')
for ep in app.url_map.iter_rules():
bp_name = ep.endpoint.split('.')[0]
if bp_name == bp:
log.debug(f'Endpoint: {ep} methods={ep.methods}')
Here is the example with a function that takes only the name of the blueprint from which you need to get its url_map:
def log_blueprint_urls(app: Flask, bp_name: str):
log = create_logger(app)
with app.app_context():
"""
Maps every single endpoint for an specific blueprint in the main application.
Also shows methos available for each endpoint
"""
bps = app.blueprints
if bp_name in bps:
log.info(f'MAP ROUTER FOR BLUEPRINT "{bp_name}"')
for ep in app.url_map.iter_rules():
bp_name = ep.endpoint.split('.')[0]
if bp_name == bp_name:
log.debug(f'Endpoint: {ep} methods={ep.methods}')
else:
log.critical(
f'BLUEPRINT "{bp_name}" has not registered in main application')

URL building with Flask and non-unique handler names

Flask provides a url_for function to generate URLs to handlers based on the URL pattern. But this would imply that the handler functions must have unique names across the entire application. Is that correct?
Example
Module A has a handler index:
#app.route('/')
def index(): pass
And Module B has another handler index:
#app.route('/anotherindex')
def index(): pass
How to distinguish the handlers called index when building URLs?
url_for('index')
I don't know how you could do with all the views routed by the same module.
What I usually do is separate my views in different modules (like you did with module A and B), and register them as blueprints, after that, when using the url_for() function, you can prefix the view name with your blueprint name and then avoid conflicts and potential problems.
Here is an example:
main_views.py:
from flask import Blueprint
main = Blueprint('main', __name__)
#main.route('/')
def index():
pass
admin_views.py:
from flask import Blueprint
admin = Blueprint('admin', __name__)
#admin.route('/admin')
def index():
pass
application.py:
from flask import Flask
from main_views import main
from admin_views import admin
app = Flask('my_application')
app.register_blueprint(main)
app.register_blueprint(admin)
Now, to access the 2 index views and still distinguish one from the other, just use url_for('main.index') or url_for('admin.index')
EDIT:
Just one more useful details about routing using blueprints, when registering the blueprint, you can pass a url_prefix argument, that will apply to every view within this blueprint.
For example, given the following code:
admin_views.py
from flask import Blueprint
admin = Blueprint('admin', __name__)
#admin.route('/')
def index():
pass
#admin.route('/logout')
def logout():
pass
application.py:
from flask import Flask
from admin_views import admin
app = Flask('my_application')
app.register_blueprint(admin, url_prefix='/admin')
The 2 views would be available at the URL /admin/ and /admin/logout

Categories