How to capture arbitrary paths at one route in FastAPI? - python

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.

Related

How to pass data from one view to another one in Fastapi?

I have a variable set in one view in Fastapi and want to pass it to another one :
from fastapi import APIRouter, Request, Response
from fastapi.templating import Jinja2Templates
templates = Jinja2Templates(directory="templates")
router = APIRouter()
#router.get("/my-first-view")
async def function1(request: Request) -> Response:
"""Display the home page."""
my_variable = value
return templates.TemplateResponse(
"home.jinja",
context={
"my_variable": my_variable
},
)
#router.get("/my-second-view")
async def function2(request: Request, my_variable: str) -> Response:
"""Display the variable processing page."""
return templates.TemplateResponse(
"page.jinja"
)
Normally, this would come to send my_variable from home.jinja to page.jinja.
Thus, in home.jinja I have the following :
...
Connect
...
But this is throwing me an error : "starlette.routing.NoMatchFound: No route exists for name \"function2\" and params \"my_variable\".\n". I did some researches but I haven't found something really helpful
What is the proper way to do it with Fastapi ? What am I missing ?
Very little context information on this post, so I'll help out with fixes that change as little as possible.
First of all, to fix your code you need a place to save the changes that you've made. A variable that only exists in a function is deleted at the end of that function.
Now usually you'd give more information regarding your use case, and I'd give you the best choice and why. It could be to store data in a JSON file, or a Database, or an in memory object that would serve the purpose best.
Here there's no info so we'll just make a variable. Keep in mind that this variable will be reset every time you restart the shell. So if your API is not "always on" then this may no work.
from fastapi import APIRouter, Request, Response
from fastapi.templating import Jinja2Templates
VARIABLE = None
API_URL = "my_domain.com/api"
templates = Jinja2Templates(directory="templates")
router = APIRouter()
#router.get("/my-first-view")
async def function1(request: Request) -> Response:
"""Display the home page."""
my_variable = value
return templates.TemplateResponse(
"home.jinja",
context={
"complete_url": f"{API_URL}/my-second-view?my_variable={VARIABLE}"
},
)
#router.get("/my-second-view")
async def function2(request: Request, my_variable: str) -> Response:
"""Display the variable processing page."""
VARIABLE = my_variable
return templates.TemplateResponse(
"page.jinja"
)
As far as the template goes, you haven't shared url_for, but I suppose it just creates the path. I'd just make home.jinja as follows.
Connect
Also I've replaced the url_for call with a simple f-string.
Depending on what you pass in your VARIABLE you may need to encode it.
For an example of that, please look at the following thread: How to urlencode a querystring in Python?

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.

FastAPI: update route docs without app restart

I have a FastAPI application where routes and their docs are defined by data in database. Simplified example of main.py:
from fastapi import FastAPI
from fastapi.routing import APIRouter
app = FastAPI()
routes = ['user', 'admin']
responses = {200: {'description': 'test'}}
def create_router(route):
router = APIRouter()
#app.get(f'/{route}', tags=[route], responses=responses)
def get_objects():
return {'foo': 'bar'}
# define other routes
app.include_router(router)
for r in routes:
create_router(s)
There is a POST route which updates data and therefore should update API docs without restarting the application. Currently I do it somehow like this:
# Route definition inside create_router
#app.post(f'/{route}/', tags=[route])
def create_object():
global responses
responses = {200: {'description': 'test2'}}
create_router(route)
return 42
It seems to work the way I want it to, but I don't like the fact that I am recreating the whole router when I need to update just the docs on one or couple of routes. Is it possible to update just docs?

Authentication verification in Python based GraphQL Server using FastAPI

I am having trouble implementing authentication verification in an GraphQL Server built with FastAPI. Before, we were using REST but now we are switching to GraphQL and I am wondering how I can implement this. Before, we had different routers and with FastAPI it is easy to check authentication based on routes using dependencies as in here. We are sending a Token in the Authorization Header which we are decoding in the backend and getting back the user_id which we can then use in our different endpoints.
I am wondering how this might work using GraphQL here. We use the Graphene and I had a look at Starlettes Authentication Examples as well as there intro into setting up GraphQl
import binascii
from fastapi import FastAPI
from starlette.authentication import (
AuthenticationBackend, AuthenticationError, SimpleUser, AuthCredentials
)
from starlette.graphql import GraphQLApp
from starlette.middleware import Middleware
from starlette.middleware.authentication import AuthenticationMiddleware
from schemas.root import my_schema
class BasicAuthBackend(AuthenticationBackend):
async def authenticate(self, request):
if "Authorization" not in request.headers:
raise AuthenticationError('No auth credentials')
auth = request.headers["Authorization"]
try:
id_token = auth.split('Bearer ')[1]
decoded_token = auth.verify_id_token(id_token)
except (ValueError, UnicodeDecodeError, binascii.Error) as exc:
raise AuthenticationError('Invalid basic auth credentials')
user_id = decoded_token['uid']
return AuthCredentials(["authenticated"]), user_id
middleware = [
Middleware(AuthenticationMiddleware, backend=BasicAuthBackend())
]
my_schema = Schema(
query=RootQuery,
mutation=RootMutation,
)
api = FastAPI(title=f"MyGraphQLServer", middleware=middleware)
api.add_route("/graphql", GraphQLApp(schema=my_schema))
For example, imagine that I now would only like to authenticate mutation Requests but not query requests. Furthermore I want to access the user_id in each of my resolvers. What would be the best way to do this?
In FastAPI documentation or starlette documentation they use add_route, that is the way to add a route in Starlette without declaring the specific operation (as would be with .get(), .post(), etc). But it has some disadvantages, we can't add dependencies like we do in FastAPI, example below
app.add_route(
"/graphql",
GraphQLApp(schema=graphene.Schema(query=Query),
executor_class=AsyncioExecutor),
dependencies=(Depends(SomeAuthorizationStuffHere)),
)
So we need to do in FastAPI, i created a simple app with HTTPBasicAuth, you can expand this with other Method's you just need to include the router(s)
from fastapi import Query, Depends, Request, FastAPI, APIRouter
from fastapi.security import HTTPBasic, HTTPBasicCredentials
import graphene
from graphene import Field, Schema, String, ObjectType
from starlette.graphql import GraphQLApp
router = APIRouter()
app = FastAPI()
security = HTTPBasic()
class Query(ObjectType):
hello = Field(String, name=String())
def resolve_hello(root, info, name):
coverage = info.context["request"].state.some_dep
return f"Hello {some_dep.some_method(name)}"
graphql_app = GraphQLApp(schema=Schema(query=Query))
#router.api_route("/gql", methods=["GET", "POST"])
async def graphql(request: Request):
return await graphql_app.handle_graphql(request=request)
app.include_router(router, dependencies=[Depends(security)])

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')

Categories