FastAPI: update route docs without app restart - python

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?

Related

How do I define a base endpoint for my Python Flask application with swagger?

I'm trying to build a simple Flask application with Swagger. So far, after a few tutorials this is what my main.py looks like:
import logging
import os
import random
from flask_restx import Api, Resource, fields
from src import create_app
app = create_app(os.getenv('env'))
api = Api(app, version='1.0', title='Sample API',
description='A sample API', doc='/api')
ns = api.namespace('userinfo', description='Python Microservice')
user = api.model('user', {
'name': fields.String(required=True, description='Employee Name'),
'role': fields.String(required=True, description='Employee Role'),
})
USERS = {'user_1000': dict(name="Elizabeth Lemon", role="Head"), 'user_1001': dict(name="Jack Donaghy", role="CEO"),
'user_1003': dict(name="Kenneth Parcell", role="Page")}
resource_fields = api.model('Resource', {
'name': fields.String,
'role': fields.String
})
#ns.route('/')
#api.doc(responses={200: 'Success', 201: 'Created'})
class UserInfo(Resource):
"""View list of all users, POST to add new user"""
def get(self):
"""List all users"""
return [{'id': u_id, 'user_info': user_info} for u_id, user_info in USERS.items()]
#api.doc(body=resource_fields)
def post(self):
"""Create a new user"""
data = api.payload
user_id = f"user_{random.randint(1004, 9999)}"
USERS[user_id] = dict(name=data['name'], role=data['role'])
logging.info(f"Created user {user_id} for {data['name']}")
return dict(user_id=user_id), 201
if __name__ == '__main__':
app.run(debug=True)
The create_app() function is defined in an __init__.py as follows:
def create_app(config_name):
app = Flask(__name__)
app.config.from_object(config_by_name[config_name])
setup_logging(app)
return app
Now, when I head over to localhost:5000/api I see the swagger page load correctly with the GET/POST documentation. However, when I hit localhost:5000 I get a URL not found error - understandable since I have not defined a / route yet - which is where I'm stuck!! How do I define a / route in my main.py? From what I understand, all endpoints currently served are /userinfo/(GET, POST) and /api. I want to add that / endpoint to facilitate a simple check of whether the application is started up
I'm new to building microservices with Flask in Python. Integrating swagger actually changed the face of main.py coz earlier w/o swagger, all my routes were annotated with app.route('/') so I could have a / endpoint.
By default, flask-restx uses namespace name to construct URLs (/<name> prefix), but you can specify a prefix to all URLs explicitly via the path argument. Something like:
ROOT_NS = api.namespace(
'section-name-that-will-be-displayed-at-swagger--fill-free-to-rename',
path='/'
)

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 session don't persist data

I have a Javascript application and a Flask application. When the user send data from Js to Flask, I store it on session and it works fine at a specific route:
#app.route(...)
def user(...):
session['name'] = name
print(session['name']) # Works !
But when I tr to get the values on session from another method / route the session is empty:
#app.route(...)
def current():
print(session.keys(), session.values) # Empty !
I have installed Flask Session and set the config to:
'SECRET_KEY': b'...',
'SESSION_TYPE': 'filesystem', # Memcache, null and redis
'SESSION_PERMANENT': False, # True
And then started the Flask application and it not work. I have also try to set session.modified = True after I add some new value to session and still not work.
I have read lots of threads on Stack Over Flow, Reddit, etc; and nothing worked. Tips please ?
TL;DR, enable CORS and credentials support on the back end, and use credentials in the front end code when issuing requests.
I recently ran into a similar issue where I was developing a front end and a back end in separate apps. I noticed that each time I issued a request from the front end client, it would create a new session for each request, which would rapidly bloat the session storage on the back end and made user tracking difficult if not impossible.
I'm assuming that you're Javascript app and Flask app are running separately (i.e., the javascript is not on a template being served by the Flask app and hence the js requests are coming from a different origin).
Suppose we have a simple app with Flask-Session enabled running on port 5000:
from flask import Flask, session
from flask_session import Session
app = Flask(__name__)
SECRET_KEY = "changeme"
SESSION_TYPE = 'filesystem'
app.config.from_object(__name__)
Session(app)
#app.route('/foo')
def foo():
return session.sid
#app.route('/bar')
def bar():
return session.sid
Now if we run the app if we navigate to either route on a browser(e.g., http://localhost:5000/foo), we would get the same session id. If you open another tab, open the developer tools and issue the following command in the console, you'd get a cors error:
// Using fetch, you can use jquery or axios
fetch("http://localhost:5000/foo").then(response => {
return response.text()
}).then(data => {
console.log(data)
})
You can fix this easily by installing Flask-CORS and wrapping your app in the CORS class:
from flask import Flask, session
from flask_session import Session
from flask_cors import CORS
app = Flask(__name__)
SECRET_KEY = "changeme"
SESSION_TYPE = 'filesystem'
app.config.from_object(__name__)
Session(app)
CORS(app)
#app.route('/foo')
def foo():
return session.sid
#app.route('/bar')
def bar():
return session.sid
Now if you run the javascript fetch function above, it prints out a different session id each time the request is invoked, even for the same route. That's because Flask can't track the session unless you're issuing the requests from the same origin or unless you provide some way for flask to identify the session. You can do this from your JS by allowing credentials to be passed:
fetch("http://localhost:5000/foo",
{ credentials: 'include' }).then(response => {
return response.text()
}).then(data => {
console.log(data)
})
However, you will get another CORS error regarding Access-Control-Allow-Credentials. You can fix this in you're Flask app by import the cross_origin decorator, wrapping your routes in the decorator and passing supports_credentials=True to the decorator. The flask code would look something like this:
from flask import Flask, session
from flask_session import Session
from flask_cors import CORS, cross_origin
app = Flask(__name__)
SECRET_KEY = "changeme"
SESSION_TYPE = 'filesystem'
app.config.from_object(__name__)
Session(app)
CORS(app)
#app.route('/foo')
#cross_origin(supports_credentials=True)
def foo():
return session.sid
#app.route('/bar')
#cross_origin(supports_credentials=True)
def bar():
return session.sid
Now flask can track the session by the requester (in this case, the browser running the Javascript app).
I had the same problem using classic post request in html. The session, which was still storing values in previous route, would empty itself after my post request.
I solved this using:
app.config.update(SESSION_COOKIE_SAMESITE="None", SESSION_COOKIE_SECURE=True)
I am sharing this in case others are facing the same issue.

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

Prevent Flask from ever sending Set-Cookie?

Can I prevent Flask framework from ever sending a Set-Cookie header?
I'm using a variety of blueprints that use the session cookie. I'm wondering if there is a way to tell the framework to simply never try to set cookies. I'd like to not have to prepare each individual response using suggestions like this or using app.after_request.
You can create custom session interface and override should_set_cookie method
from flask import Flask
from flask.sessions import SecureCookieSessionInterface, SessionMixin
class CustomSessionInterface(SecureCookieSessionInterface):
def should_set_cookie(self, app: "Flask", session: SessionMixin) -> bool:
return False
app = Flask(__name__)
app.session_interface = CustomSessionInterface()

Categories