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?
Related
In the Flask framework, I would like to be able to determine which request route would be selected to serve a URL, without having to actually request the URL itself. For example, if I have an app like:
import flask
app = flask.Flask(__name__)
#app.route('/')
def index():
pass
#app.route('/<id:int>')
#app.route('/<id:int>-<slug>')
def show_page(id, slug = None):
pass
then I'd like to be able to do something like (fictional example):
route = app.get_url_rule("/12345")
and get a result like:
('show_page', {'id':12345,'slug':None})
As far as I can tell, Flask doesn't expose any functionality like this on its own, but is there a way to possibly dig down into its request routing layer (or possibly Werkzeug) to implement this myself?
The result you're looking for is exactly what Flask does for routing, through Werkzeug. When you decorate view functions, the rules are registered in an instance of werkzeug.routing.Map. When the app handles a request, the map is bound to the request to create a MapAdapter. The adapter has a match() method which either returns an endpoint, args match or raises a routing error such as 307, 404, or 405. Flask call this and uses the result to call the appropriate view function.
Call app.create_url_adapter() to get an adapter based on the app configuration. When not in a request, this requires configuring SERVER_NAME. Then call adapter.match(url) to get a match. This example ignores routing exceptions and returns None.
from werkzeug.exceptions import HTTPException
# remember to configure app.config["SERVER_NAME"]
def match_url(url):
adapter = app.get_url_adapter()
try:
return adapter.match(url)
except HTTPException:
return None
Calling match_url("/12345-example") returns ('show_page', {'id': 12345, 'slug': 'example'}).
Note that this normally isn't useful, as the point of Flask is that it will already do the routing and call the appropriate view.
I tried Davidism's method with your route code, in case this helps...
from flask import Flask, request
app = Flask(__name__)
#app.route('/<int:id>')
#app.route('/<int:id>-<slug>')
def show_page(id, slug = None):
adapter = app.create_url_adapter(request)
endpoint, values = adapter.match()
# Return this to frontend of ease of demo
return {endpoint: values}
A request to /123 returns:
{
"show_page": {
"id": 123
}
}
A request to /123-abc returns:
{
"show_page": {
"id": 123,
"slug": "abc"
}
}
I don't do much web work but I recently began using FastAPI and am building an MVC app with jinja2 templating that uses PowerBI embedded capacity to serve multiple embedded analytics in app owns data arrangement. All of this works beautifully. However, I'm wanting to add further modules and I'd like to use the msal package to do user authentication by routing a user to the Microsoft login page, letting them sign in against a multi-tenant app service I set up in Azure, and then redirecting back to my page via redirect URI, grabbing the token, and progressing with authorization. Microsoft saved a great example our here for doing this in Flask. However, I am having fits porting the example to FastAPI.
I can get the user to the login screen and log in but I am having no luck capturing the token at my call back URI - it's appropriately routing but I am unable to capture the token from the response.
Has anyone (or can anyone) taken that super simple Flask example and ported it to FastAPI? Everything I find online for FAPI is back-end token-bearer headers for APIs - not meant for MVC apps.
Here's my current code. Messy because I have "tests" built in.
import msal
import requests
from fastapi import APIRouter, Request, Response
from fastapi.responses import RedirectResponse
from starlette.templating import Jinja2Templates
from config import get_settings
settings = get_settings()
router = APIRouter()
templates = Jinja2Templates('templates')
# Works
#router.get('/login', include_in_schema=False)
async def login(request: Request):
request.session['flow'] = _build_auth_code_flow(scopes=settings.AUTH_SCOPE)
login_url = request.session['flow']['auth_uri']
return templates.TemplateResponse('error.html', {'request': request, 'message': login_url})
# DOES NOT WORK - Pretty sure error is in here --------------------
#router.get('/getAToken', response_class=Response, include_in_schema=False)
async def authorize(request: Request):
try:
cache = _load_cache(request)
result = _build_msal_app(cache=cache).acquire_token_by_auth_code_flow(
request.session.get('flow'), request.session
)
if 'error' in result:
return templates.TemplateResponse('error.html', {'request': request, 'message': result})
request.session['user'] = result.get('id_token_claims')
_save_cache(cache)
except Exception as error:
return templates.TemplateResponse('error.html', {'request': request, 'message': f'{error}: {str(request.query_params)}'})
return templates.TemplateResponse('error.html', {'request': request, 'message': result})
# -----------------------------------------------------
def _load_cache(request: Request):
cache = msal.SerializableTokenCache()
if request.session.get("token_cache"):
cache.deserialize(request.session["token_cache"])
return cache
def _save_cache(request: Request, cache):
if cache.has_state_changed:
request.session["token_cache"] = cache.serialize()
def _build_msal_app(cache=None, authority=None):
return msal.ConfidentialClientApplication(
settings.CLIENT_ID,
authority=authority or settings.AUTH_AUTHORITY,
client_credential=settings.CLIENT_SECRET,
token_cache=cache
)
def _build_auth_code_flow(authority=None, scopes=None):
return _build_msal_app(authority=authority).initiate_auth_code_flow(
scopes or [],
redirect_uri=settings.AUTH_REDIRECT)
def _get_token_from_cache(scope=None):
cache = _load_cache() # This web app maintains one cache per session
cca = _build_msal_app(cache=cache)
accounts = cca.get_accounts()
if accounts: # So all account(s) belong to the current signed-in user
result = cca.acquire_token_silent(scope, account=accounts[0])
_save_cache(cache)
return result
Any help is GREATLY appreciated. Happy to answer any questions. Thank you.
This is because FastAPI session variables are stored client-side as a cookie, which has a limit of 4096 bytes of data. The data being stored from the redirect url is pushes the cookie size over this limit and results in the data not being stored. Starlette-session is an alternative SessionMiddleware that stores variables server-side, eliminating cookie limit. Below is a basic (but messy) implementation:
from fastapi import FastAPI
from fastapi.templating import Jinja2Templates
from starlette.requests import Request
from starlette.responses import RedirectResponse
from starlette_session import SessionMiddleware
from starlette_session.backends import BackendType
from redis import Redis
import uvicorn
import functools
import msal
app_client_id = "sample_msal_client_id"
app_client_secret = "sample_msal_client_secret"
tenant_id = "sample_msal_tenant_id"
app = FastAPI()
redis_client = Redis(host="localhost", port=6379)
app.add_middleware(
SessionMiddleware,
secret_key="SECURE_SECRET_KEY",
cookie_name="auth_cookie",
backend_type=BackendType.redis,
backend_client=redis_client,
)
templates = Jinja2Templates(directory="templates")
default_scope = ["https://graph.microsoft.com/.default"]
token_cache_key = "token_cache"
# Private Functions - Start
def _load_cache(session):
cache = msal.SerializableTokenCache()
if session.get(token_cache_key):
cache.deserialize(session[token_cache_key])
return cache
def _save_cache(cache,session):
if cache.has_state_changed:
session[token_cache_key] = cache.serialize()
def _build_msal_app(cache=None):
return msal.ConfidentialClientApplication(
app_client_id,
client_credential=app_client_secret,
authority=f"https://login.microsoftonline.com/{tenant_id}",
token_cache=cache
)
def _build_auth_code_flow(request):
return _build_msal_app().initiate_auth_code_flow(
default_scope, #Scopes
redirect_uri=request.url_for("callback") #Redirect URI
)
def _get_token_from_cache(session):
cache = _load_cache(session) # This web app maintains one cache per session
cca = _build_msal_app(cache=cache)
accounts = cca.get_accounts()
if accounts: # So all account(s) belong to the current signed-in user
result = cca.acquire_token_silent(default_scope, account=accounts[0])
_save_cache(cache,session)
return result
# Private Functions - End
# Custom Decorators - Start
def authenticated_endpoint(func):
#functools.wraps(func)
def is_authenticated(*args,**kwargs):
try:
request = kwargs["request"]
token = _get_token_from_cache(request.session)
if not token:
return RedirectResponse(request.url_for("login"))
return func(*args,**kwargs)
except:
return RedirectResponse(request.url_for("login"))
return is_authenticated
# Custom Decorators - End
# Endpoints - Start
#app.get("/")
#authenticated_endpoint
def index(request:Request):
return {
"result": "good"
}
#app.get("/login")
def login(request:Request):
return templates.TemplateResponse("login.html",{
"version": msal.__version__,
'request': request,
"config": {
"B2C_RESET_PASSWORD_AUTHORITY": False
}
})
#app.get("/oauth/redirect")
def get_redirect_url(request:Request):
request.session["flow"] = _build_auth_code_flow(request)
return RedirectResponse(request.session["flow"]["auth_uri"])
#app.get("/callback")
async def callback(request:Request):
cache = _load_cache(request.session)
result = _build_msal_app(cache=cache).acquire_token_by_auth_code_flow(request.session.get("flow", {}), dict(request.query_params))
if "error" in result:
return templates.TemplateResponse("auth_error.html",{
"result": result,
'request': request
})
request.session["user"] = result.get("id_token_claims")
request.session[token_cache_key] = cache.serialize()
return RedirectResponse(request.url_for("index"))
# Endpoints - End
if __name__ == "__main__":
uvicorn.run("main:app",host='0.0.0.0', port=4557,reload=True)`
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.
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()
On Successfully POSTing to a form endpoint I redirect back to the same endpoint with some URL params that my client side code can interact with.
#bp.route('/submit', methods=['GET', 'POST'])
def submit():
form = SubmissionForm()
labels = current_app.config['TRELLO_LABELS']
if form.validate_on_submit():
submission = Submission().create(
title=form.data['title'], email=form.data['email'], card_id=card.id, card_url=card.url)
# reset form by redirecting back and setting the URL params
return redirect(url_for('bp.submit', success=1, id=card.id))
return render_template('submit.html', form=form)
But I ran into some issues trying to write a test for this code as I can't figure out how to test that those URL params are on my redirect URL. My incomplete test code is:
import pytest
#pytest.mark.usefixtures('session')
class TestRoutes:
def test_submit_post(self, app, mocker):
with app.test_request_context('/submit',
method='post',
query_string=dict(
email='email#example.com',
title='foo',
pitch='foo',
format='IN-DEPTH',
audience='INTERMEDIATE',
description='foo',
notes='foo')):
assert resp.status_code == 200
I've tried a few different methods to test this. With and without the context manager and I've dug deep into the Flask and Werkzeug source on the test_client and test_request_context.
I just want to test that the URL params for success and id exist on redirect after a valid POST.
Here's a super simple yet inclusive example of patching Flask's url_for method (can be run as-is in a Python interpreter):
import flask
from unittest.mock import patch
#patch('flask.url_for')
def test(self):
resp = flask.url_for('spam')
self.assert_called_with('spam')
However, the above example will only work if you're importing Flask directly and not using from flask import url_for in your routes code. You'll have to patch the exact namespace, which would look something like this:
#patch('application.routes.url_for')
def another_test(self, client):
# Some code that envokes Flask's url_for, such as:
client.post('/submit', data={}, follow_redirects=True)
self.assert_called_once_with('bp.submit', success=1, id=1)
For more info, check out Where to Patch in the mock documentation.
You could use mock's function patch function to patch url_for capturing the provided arguments and then test against them.